"Fossies" - the Fresh Open Source Software Archive

Member "nmap-7.91/nselib/ldap.lua" (10 Oct 2020, 32770 Bytes) of package /linux/misc/nmap-7.91.tgz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "ldap.lua": 7.90_vs_7.91.

    1 ---
    2 -- Library methods for handling LDAP.
    3 --
    4 -- @author Patrik Karlsson
    5 -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
    6 --
    7 -- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
    8 --
    9 -- Version 0.8
   10 -- Created 01/12/2010 - v0.1 - Created by Patrik Karlsson <patrik@cqure.net>
   11 -- Revised 01/28/2010 - v0.2 - Revised to fit better fit ASN.1 library
   12 -- Revised 02/02/2010 - v0.3 - Revised to fit OO ASN.1 Library
   13 -- Revised 09/05/2011 - v0.4 - Revised to include support for writing output to file, added decoding certain time
   14 --                             formats
   15 -- Revised 10/29/2011 - v0.5 - Added support for performing wildcard searches via the substring filter.
   16 -- Revised 10/30/2011 - v0.6 - Added support for the ldap extensibleMatch filter type for searches
   17 -- Revised 04/04/2016 - v0.7 - Added support for searchRequest over upd ( udpSearchRequest ) - Tom Sellers
   18 -- Revised 07/11/2017 - v0.8 - Added support for decoding the objectSID Active Directory attribute - Tom Sellers
   19 --
   20 
   21 local asn1 = require "asn1"
   22 local datetime = require "datetime"
   23 local io = require "io"
   24 local nmap = require "nmap"
   25 local stdnse = require "stdnse"
   26 local string = require "string"
   27 local stringaux = require "stringaux"
   28 local table = require "table"
   29 local comm = require "comm"
   30 _ENV = stdnse.module("ldap", stdnse.seeall)
   31 
   32 local ldapMessageId = 1
   33 
   34 ERROR_MSG = {}
   35 ERROR_MSG[1]  = "Initialization of LDAP library failed."
   36 ERROR_MSG[4]  = "Size limit exceeded."
   37 ERROR_MSG[13] = "Confidentiality required"
   38 ERROR_MSG[32] = "No such object"
   39 ERROR_MSG[34] = "Invalid DN"
   40 ERROR_MSG[49] = "The supplied credential is invalid."
   41 
   42 ERRORS = {
   43   LDAP_SUCCESS = 0,
   44   LDAP_SIZELIMIT_EXCEEDED = 4
   45 }
   46 
   47 --- Application constants
   48 -- @class table
   49 -- @name APPNO
   50 APPNO = {
   51   BindRequest = 0,
   52   BindResponse = 1,
   53   UnbindRequest = 2,
   54   SearchRequest = 3,
   55   SearchResponse = 4,
   56   SearchResDone = 5
   57 }
   58 
   59 -- Filter operation constants
   60 FILTER = {
   61   _and = 0,
   62   _or = 1,
   63   _not = 2,
   64   equalityMatch = 3,
   65   substrings = 4,
   66   greaterOrEqual = 5,
   67   lessOrEqual = 6,
   68   present = 7,
   69   approxMatch = 8,
   70   extensibleMatch = 9
   71 }
   72 
   73 -- Scope constants
   74 SCOPE = {
   75   base=0,
   76   one=1,
   77   sub= 2,
   78   children=3,
   79   default = 0
   80 }
   81 
   82 -- Deref policy constants
   83 DEREFPOLICY = {
   84   never=0,
   85   searching=1,
   86   finding = 2,
   87   always=3,
   88   default = 0
   89 }
   90 
   91 -- LDAP specific tag encoders
   92 local tagEncoder = {}
   93 
   94 tagEncoder['table'] = function(self, val)
   95   if (val._ldap == '\x0A') then
   96     local ival = self.encodeInt(val[1])
   97     local len = self.encodeLength(#ival)
   98     return val._ldap .. len .. ival
   99   end
  100   if (val._ldaptype) then
  101     local len
  102     if val[1] == nil or #val[1] == 0 then
  103       return val._ldaptype .. '\0'
  104     else
  105       len = self.encodeLength(#val[1])
  106       return val._ldaptype .. len .. val[1]
  107     end
  108   end
  109 
  110   local encVal = ""
  111   for _, v in ipairs(val) do
  112     encVal = encVal .. encode(v) -- todo: buffer?
  113   end
  114   local tableType = val._snmp or "\x30"
  115   return tableType .. self.encodeLength(#encVal) .. encVal
  116 
  117 end
  118 
  119 ---
  120 -- Encodes a given value according to ASN.1 basic encoding rules for SNMP
  121 -- packet creation.
  122 -- @param val Value to be encoded.
  123 -- @return Encoded value.
  124 function encode(val)
  125 
  126   local encoder = asn1.ASN1Encoder:new()
  127   local encValue
  128 
  129   encoder:registerTagEncoders(tagEncoder)
  130   encValue = encoder:encode(val)
  131 
  132   if encValue then
  133     return encValue
  134   end
  135 
  136   return ''
  137 end
  138 
  139 
  140 -- LDAP specific tag decoders
  141 local tagDecoder = {}
  142 
  143 tagDecoder["\x0A"] = function( self, encStr, elen, pos )
  144   return self.decodeInt(encStr, elen, pos)
  145 end
  146 
  147 tagDecoder["\x8A"] = function( self, encStr, elen, pos )
  148   return string.unpack("c" .. elen, encStr, pos)
  149 end
  150 
  151 -- null decoder
  152 tagDecoder["\x31"] = function( self, encStr, elen, pos )
  153   return nil, pos
  154 end
  155 
  156 
  157 ---
  158 -- Decodes an LDAP packet or a part of it according to ASN.1 basic encoding
  159 -- rules.
  160 -- @param encStr Encoded string.
  161 -- @param pos Current position in the string.
  162 -- @return The decoded value(s).
  163 -- @return The position after decoding
  164 function decode(encStr, pos)
  165   -- register the LDAP specific tag decoders
  166   local decoder = asn1.ASN1Decoder:new()
  167   decoder:registerTagDecoders( tagDecoder )
  168   return decoder:decode( encStr, pos )
  169 end
  170 
  171 
  172 ---
  173 -- Decodes a sequence according to ASN.1 basic encoding rules.
  174 -- @param encStr Encoded string.
  175 -- @param len Length of sequence in bytes.
  176 -- @param pos Current position in the string.
  177 -- @return The decoded sequence as a table.
  178 -- @return The position after decoding.
  179 local function decodeSeq(encStr, len, pos)
  180   local seq = {}
  181   local sPos = 1
  182   if #encStr - pos + 1 < len then
  183     return seq, nil
  184   end
  185   local sStr, newpos = string.unpack("c" .. len, encStr, pos)
  186   while (sPos < len) do
  187     local newSeq
  188     newSeq, sPos = decode(sStr, sPos)
  189     table.insert(seq, newSeq)
  190   end
  191   return seq, newpos
  192 end
  193 
  194 -- Encodes an LDAP Application operation and its data as a sequence
  195 --
  196 -- @param appno LDAP application number
  197 -- @see APPNO
  198 -- @param isConstructed boolean true if constructed, false if primitive
  199 -- @param data string containing the LDAP operation content
  200 -- @return string containing the encoded LDAP operation
  201 function encodeLDAPOp( appno, isConstructed, data )
  202   local encoded_str = ""
  203   local asn1_type = asn1.BERtoInt( asn1.BERCLASS.Application, isConstructed, appno )
  204 
  205   encoded_str = encode( { _ldaptype = string.pack("B", asn1_type), data } )
  206   return encoded_str
  207 end
  208 
  209 --- Performs an LDAP Search request
  210 --
  211 -- This function has a concept of softerrors which populates the return tables error information
  212 -- while returning a true status. The reason for this is that LDAP may return a number of records
  213 -- and then finish off with an error like SIZE LIMIT EXCEEDED. We still want to return the records
  214 -- that were received prior to the error. In order to achieve this and not terminating the script
  215 -- by returning a false status a true status is returned together with a table containing all searchentries.
  216 -- This table has the <code>errorMessage</code> and <code>resultCode</code> entries set with the error information.
  217 -- As a <code>try</code> won't catch this error it's up to the script to do so. See ldap-search.nse for an example.
  218 --
  219 -- @param socket socket already connected to the ldap server
  220 -- @param params table containing at least <code>scope</code>, <code>derefPolicy</code>, <code>baseObject</code>
  221 --        the field <code>maxObjects</code> may also be included to restrict the amount of records returned
  222 -- @return success true or false.
  223 -- @return searchResEntries containing results or a string containing error message
  224 function searchRequest( socket, params )
  225 
  226   local searchResEntries = { errorMessage="", resultCode = 0}
  227   local catch = function() socket:close() stdnse.debug1("SearchRequest failed") end
  228   local try = nmap.new_try(catch)
  229   local attributes = params.attributes
  230   local request = encode(params.baseObject)
  231   local attrSeq = ''
  232   local requestData, messageSeq, data
  233   local maxObjects = params.maxObjects or -1
  234 
  235   local encoder = asn1.ASN1Encoder:new()
  236   local decoder = asn1.ASN1Decoder:new()
  237 
  238   encoder:registerTagEncoders(tagEncoder)
  239   decoder:registerTagDecoders(tagDecoder)
  240 
  241   request = request .. encode( { _ldap='\x0A', params.scope } )--scope
  242   request = request .. encode( { _ldap='\x0A', params.derefPolicy } )--derefpolicy
  243   request = request .. encode( params.sizeLimit or 0)--sizelimit
  244   request = request .. encode( params.timeLimit or 0)--timelimit
  245   request = request .. encode( params.typesOnly or false)--TypesOnly
  246 
  247   if params.filter then
  248     request = request .. createFilter( params.filter )
  249   else
  250     request = request .. encode( { _ldaptype='\x87', "objectclass" } )-- filter : string, presence
  251   end
  252   if  attributes~= nil then
  253     for _,attr in ipairs(attributes) do
  254       attrSeq = attrSeq .. encode(attr)
  255     end
  256   end
  257 
  258   request = request .. encoder:encodeSeq(attrSeq)
  259   requestData = encodeLDAPOp(APPNO.SearchRequest, true, request)
  260   messageSeq = encode(ldapMessageId)
  261   ldapMessageId = ldapMessageId +1
  262   messageSeq = messageSeq .. requestData
  263   data = encoder:encodeSeq(messageSeq)
  264   try( socket:send( data ) )
  265   data = ""
  266 
  267   while true do
  268     local len, pos, messageId = 0, 2, -1
  269     local tmp = ""
  270     local _, objectName, attributes, ldapOp
  271     local attributes
  272     local searchResEntry = {}
  273 
  274     if ( maxObjects == 0 ) then
  275       break
  276     elseif ( maxObjects > 0 ) then
  277       maxObjects = maxObjects - 1
  278     end
  279 
  280     if data:len() > 6 then
  281       len, pos = decoder.decodeLength( data, pos )
  282     else
  283       data = data .. try( socket:receive() )
  284       len, pos = decoder.decodeLength( data, pos )
  285     end
  286     -- pos should be at the right position regardless if length is specified in 1 or 2 bytes
  287     while ( len + pos - 1 > data:len() ) do
  288       data = data .. try( socket:receive() )
  289     end
  290 
  291     messageId, pos = decode( data, pos )
  292     tmp, pos = string.unpack("B", data, pos)
  293     len, pos = decoder.decodeLength( data, pos )
  294     ldapOp = asn1.intToBER( tmp )
  295     searchResEntry = {}
  296 
  297     if ldapOp.number == APPNO.SearchResDone then
  298       searchResEntry.resultCode, pos = decode( data, pos )
  299       -- errors may occur after a large amount of data has been received (eg. size limit exceeded)
  300       -- we want to be able to return the data received prior to this error to the user
  301       -- however, we also need to alert the user of the error. This is achieved through "softerrors"
  302       -- softerrors populate the error fields of the table while returning a true status
  303       -- this allows for the caller to output data while still being able to catch the error
  304       if ( searchResEntry.resultCode ~= 0 ) then
  305         local error_msg
  306         searchResEntry.matchedDN, pos = decode( data, pos )
  307         searchResEntry.errorMessage, pos = decode( data, pos )
  308         error_msg = ERROR_MSG[searchResEntry.resultCode]
  309         -- if the table is empty return a hard error
  310         if #searchResEntries == 0 then
  311           return false, string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
  312         else
  313           searchResEntries.errorMessage = string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
  314           searchResEntries.resultCode = searchResEntry.resultCode
  315           return true, searchResEntries
  316         end
  317       end
  318       break
  319     end
  320 
  321     searchResEntry.objectName, pos = decode( data, pos )
  322     if ldapOp.number == APPNO.SearchResponse then
  323       searchResEntry.attributes, pos = decode( data, pos )
  324 
  325       table.insert( searchResEntries, searchResEntry )
  326     end
  327     if data:len() > pos then
  328       data = data:sub(pos)
  329     else
  330       data = ""
  331     end
  332   end
  333   return true, searchResEntries
  334 end
  335 
  336 --- Performs an LDAP Search request over UDP
  337 --
  338 -- This function has a concept of softerrors which populates the return tables error information
  339 -- while returning a true status. The reason for this is that LDAP may return a number of records
  340 -- and then finish off with an error like SIZE LIMIT EXCEEDED. We still want to return the records
  341 -- that were received prior to the error. In order to achieve this and not terminating the script
  342 -- by returning a false status a true status is returned together with a table containing all searchentries.
  343 -- This table has the <code>errorMessage</code> and <code>resultCode</code> entries set with the error information.
  344 -- As a <code>try</code> won't catch this error it's up to the script to do so. See ldap-search.nse for an example.
  345 --
  346 -- @param host The host to connect to
  347 -- @param port The port on the host
  348 -- @param params table containing at least <code>scope</code>, <code>derefPolicy</code>, <code>baseObject</code>
  349 --        the field <code>maxObjects</code> may also be included to restrict the amount of records returned
  350 -- @return success true or false.
  351 -- @return searchResEntries containing results or a string containing error message
  352 
  353 function udpSearchRequest( host, port, params )
  354 
  355   local searchResEntries = { errorMessage="", resultCode = 0}
  356   local catch = function() stdnse.debug1("udpSearchRequest failed") end
  357   local try = nmap.new_try(catch)
  358   local attributes = params.attributes
  359   local request = encode(params.baseObject)
  360   local attrSeq = ''
  361   local requestData, messageSeq, data
  362   local maxObjects = params.maxObjects or -1
  363 
  364   local encoder = asn1.ASN1Encoder:new()
  365   local decoder = asn1.ASN1Decoder:new()
  366 
  367   encoder:registerTagEncoders(tagEncoder)
  368   decoder:registerTagDecoders(tagDecoder)
  369 
  370   request = request .. encode( { _ldap='\x0A', params.scope } )--scope
  371   request = request .. encode( { _ldap='\x0A', params.derefPolicy } )--derefpolicy
  372   request = request .. encode( params.sizeLimit or 0)--sizelimit
  373   request = request .. encode( params.timeLimit or 0)--timelimit
  374   request = request .. encode( params.typesOnly or false)--TypesOnly
  375 
  376   if params.filter then
  377     request = request .. createFilter( params.filter )
  378   else
  379     request = request .. encode( { _ldaptype='\x87', "objectclass" } )-- filter : string, presence
  380   end
  381   if  attributes~= nil then
  382     for _,attr in ipairs(attributes) do
  383       attrSeq = attrSeq .. encode(attr)
  384     end
  385   end
  386 
  387   request = request .. encoder:encodeSeq(attrSeq)
  388   requestData = encodeLDAPOp(APPNO.SearchRequest, true, request)
  389   messageSeq = encode(ldapMessageId)
  390   ldapMessageId = ldapMessageId +1
  391   messageSeq = messageSeq .. requestData
  392   data = encoder:encodeSeq(messageSeq)
  393   local status, response = comm.exchange(host, port, data)
  394 
  395   while true do
  396     local len, pos, messageId = 0, 0, -1
  397     local tmp = ""
  398     local _, objectName, attributes, ldapOp
  399     local attributes
  400     local searchResEntry = {}
  401 
  402     if ( maxObjects == 0 ) then
  403       break
  404     elseif ( maxObjects > 0 ) then
  405       maxObjects = maxObjects - 1
  406     end
  407 
  408     tmp, pos = string.unpack("B", response, pos)
  409     len, pos = decoder.decodeLength( response, pos )
  410     messageId, pos = decode( response, pos )
  411     tmp, pos = string.unpack("B", response, pos)
  412     len, pos = decoder.decodeLength( response, pos )
  413     ldapOp = asn1.intToBER( tmp )
  414     searchResEntry = {}
  415 
  416     if ldapOp.number == APPNO.SearchResDone then
  417       searchResEntry.resultCode, pos = decode( response, pos )
  418       -- errors may occur after a large amount of response has been received (eg. size limit exceeded)
  419       -- we want to be able to return the response received prior to this error to the user
  420       -- however, we also need to alert the user of the error. This is achieved through "softerrors"
  421       -- softerrors populate the error fields of the table while returning a true status
  422       -- this allows for the caller to output response while still being able to catch the error
  423       if ( searchResEntry.resultCode ~= 0 ) then
  424         local error_msg
  425         searchResEntry.matchedDN, pos = decode( response, pos )
  426         searchResEntry.errorMessage, pos = decode( response, pos )
  427         error_msg = ERROR_MSG[searchResEntry.resultCode]
  428         -- if the table is empty return a hard error
  429         if #searchResEntries == 0 then
  430           return false, string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
  431         else
  432           searchResEntries.errorMessage = string.format("Code: %d|Error: %s|Details: %s", searchResEntry.resultCode, error_msg or "", searchResEntry.errorMessage or "" )
  433           searchResEntries.resultCode = searchResEntry.resultCode
  434           return true, searchResEntries
  435         end
  436       end
  437       break
  438     end
  439 
  440     searchResEntry.objectName, pos = decode( response, pos )
  441     if ldapOp.number == APPNO.SearchResponse then
  442       searchResEntry.attributes, pos = decode( response, pos )
  443       table.insert( searchResEntries, searchResEntry )
  444     end
  445     if response:len() > pos then
  446       response = response:sub(pos)
  447     else
  448       response = ""
  449     end
  450   end
  451   return true, searchResEntries
  452 end
  453 
  454 --- Attempts to bind to the server using the credentials given
  455 --
  456 -- @param socket socket already connected to the ldap server
  457 -- @param params table containing <code>version</code>, <code>username</code> and <code>password</code>
  458 -- @return success true or false
  459 -- @return err string containing error message
  460 function bindRequest( socket, params )
  461 
  462   local catch = function() socket:close() stdnse.debug1("bindRequest failed") end
  463   local try = nmap.new_try(catch)
  464   local ldapAuth = encode( { _ldaptype = '\x80', params.password } )
  465   local bindReq = encode( params.version ) .. encode( params.username ) .. ldapAuth
  466   local ldapMsg = encode(ldapMessageId) .. encodeLDAPOp( APPNO.BindRequest, true, bindReq )
  467   local packet
  468   local pos, packet_len, resultCode, tmp, len, _
  469   local response = {}
  470 
  471   local encoder = asn1.ASN1Encoder:new()
  472   local decoder = asn1.ASN1Decoder:new()
  473 
  474   encoder:registerTagEncoders(tagEncoder)
  475   decoder:registerTagDecoders(tagDecoder)
  476 
  477   packet = encoder:encodeSeq( ldapMsg )
  478   ldapMessageId = ldapMessageId +1
  479   try( socket:send( packet ) )
  480   packet = try( socket:receive() )
  481 
  482   packet_len, pos = decoder.decodeLength( packet, 2 )
  483   response.messageID, pos = decode( packet, pos )
  484   tmp, pos = string.unpack("B", packet, pos)
  485   len, pos = decoder.decodeLength( packet, pos )
  486   response.protocolOp = asn1.intToBER( tmp )
  487 
  488   if response.protocolOp.number ~= APPNO.BindResponse then
  489     return false, string.format("Received incorrect Op in packet: %d, expected %d", response.protocolOp.number, APPNO.BindResponse)
  490   end
  491 
  492   response.resultCode, pos = decode( packet, pos )
  493 
  494   if ( response.resultCode ~= 0 ) then
  495     local error_msg
  496     response.matchedDN, pos = decode( packet, pos )
  497     response.errorMessage, pos = decode( packet, pos )
  498     error_msg = ERROR_MSG[response.resultCode]
  499     return false, string.format("\n  Error: %s\n  Details: %s",
  500       error_msg or "Unknown error occurred (code: " .. response.resultCode ..
  501       ")", response.errorMessage or "" )
  502   else
  503     return true, "Success"
  504   end
  505 end
  506 
  507 --- Performs an LDAP Unbind
  508 --
  509 -- @param socket socket already connected to the ldap server
  510 -- @return success true or false
  511 -- @return err string containing error message
  512 function unbindRequest( socket )
  513 
  514   local ldapMsg, packet
  515   local catch = function() socket:close() stdnse.debug1("bindRequest failed") end
  516   local try = nmap.new_try(catch)
  517 
  518   local encoder = asn1.ASN1Encoder:new()
  519   encoder:registerTagEncoders(tagEncoder)
  520 
  521   ldapMessageId = ldapMessageId +1
  522   ldapMsg = encode( ldapMessageId ) .. encodeLDAPOp( APPNO.UnbindRequest, false, nil)
  523   packet = encoder:encodeSeq( ldapMsg )
  524   try( socket:send( packet ) )
  525   return true, ""
  526 end
  527 
  528 
  529 --- Creates an ASN1 structure from a filter table
  530 --
  531 -- @param filter table containing the filter to be created
  532 -- @return string containing the ASN1 byte sequence
  533 function createFilter( filter )
  534 
  535   local asn1_type = asn1.BERtoInt( asn1.BERCLASS.ContextSpecific, true, filter.op )
  536   local filter_str = ""
  537 
  538   if type(filter.val) == 'table' then
  539     for _, v in ipairs( filter.val ) do
  540       filter_str = filter_str .. createFilter( v )
  541     end
  542   else
  543     local obj = encode( filter.obj )
  544     local val = ''
  545     if ( filter.op == FILTER['substrings'] ) then
  546 
  547       local tmptable = stringaux.strsplit('*', filter.val)
  548       local tmp_result = ''
  549 
  550       if (#tmptable <= 1 ) then
  551         -- 0x81  = 10000001  =  10        0                 00001
  552         -- hex     binary       Context   Primitive value   Field: Sequence  Value: 1 (any / any position in string)
  553         tmp_result = string.pack('Bs1', 0x81, filter.val)
  554       else
  555         for indexval, substr in ipairs(tmptable) do
  556           if (indexval == 1) and (substr ~= '') then
  557             -- 0x81  = 10000000  =  10        0                 00000
  558             -- hex     binary       Context   Primitive value   Field: Sequence  Value: 0 (initial / match at start of string)
  559             tmp_result = '\x80' .. string.char(#substr) .. substr
  560           end
  561 
  562           if (indexval ~= #tmptable) and (indexval ~= 1) and (substr ~= '') then
  563             -- 0x81  = 10000001  =  10        0                 00001
  564             -- hex     binary       Context   Primitive value   Field: Sequence  Value: 1 (any / match in any position in string)
  565             tmp_result = tmp_result .. string.pack('Bs1', 0x81, substr)
  566           end
  567 
  568           if (indexval == #tmptable) and (substr ~= '') then
  569             -- 0x82  = 10000010  =  10        0                 00010
  570             -- hex     binary       Context   Primitive value   Field: Sequence  Value: 2 (final / match at end of string)
  571             tmp_result = tmp_result .. string.pack('Bs1', 0x82, substr)
  572           end
  573         end
  574       end
  575 
  576       val = asn1.ASN1Encoder:encodeSeq( tmp_result )
  577 
  578     elseif ( filter.op == FILTER['extensibleMatch'] ) then
  579 
  580       local tmptable = stringaux.strsplit(':=', filter.val)
  581       local tmp_result = ''
  582       local OID, bitmask
  583 
  584       if ( tmptable[1] ~= nil ) then
  585         OID = tmptable[1]
  586       else
  587         return false, ("ERROR: Invalid extensibleMatch query format")
  588       end
  589 
  590       if ( tmptable[2] ~= nil ) then
  591         bitmask = tmptable[2]
  592       else
  593         return false, ("ERROR: Invalid extensibleMatch query format")
  594       end
  595 
  596       tmp_result = string.pack('Bs1 Bs1 Bs1 Bs1',
  597         -- Format and create matchingRule using OID
  598         -- 0x81  = 10000001  =  10        0                 00001
  599         -- hex     binary       Context   Primitive value   Field: matchingRule  Value: 1
  600         0x81, OID,
  601 
  602         -- Format and create type using ldap attribute
  603         -- 0x82  = 10000010  =  10        0                 00010
  604         -- hex     binary       Context   Primitive value   Field: Type          Value: 2
  605         0x82, filter.obj,
  606 
  607         -- Format and create matchValue using bitmask
  608         -- 0x83  = 10000011  =  10        0                 00011
  609         -- hex     binary       Context   Primitive value   Field: matchValue    Value: 3
  610         0x83, bitmask,
  611 
  612         -- Format and create dnAttributes, defaulting to false
  613         -- 0x84  = 10000100  =  10        0                 00100
  614         -- hex     binary       Context   Primitive value   Field: dnAttributes  Value: 4
  615         -- 0x00 =  boolean value, in this case false
  616         0x84, '\x00')
  617 
  618       -- Format the overall extensibleMatch block
  619       -- 0xa9  = 10101001  =  10        1                 01001
  620       -- hex     binary       Context   Constructed       Field: Filter       Value: 9 (extensibleMatch)
  621       return '\xa9' .. asn1.ASN1Encoder.encodeLength(#tmp_result) .. tmp_result
  622 
  623     else
  624       val = encode( filter.val )
  625     end
  626 
  627     filter_str = filter_str .. obj .. val
  628 
  629   end
  630   return encode( { _ldaptype=string.pack("B", asn1_type), filter_str } )
  631 end
  632 
  633 --- Converts a search result as received from searchRequest to a "result" table
  634 --
  635 -- Does some limited decoding of LDAP attributes
  636 --
  637 -- TODO: Add decoding of missing attributes
  638 -- TODO: Add decoding of userParameters
  639 -- TODO: Add decoding of loginHours
  640 --
  641 -- @param searchEntries table as returned from searchRequest
  642 -- @return table suitable for <code>stdnse.format_output</code>
  643 function searchResultToTable( searchEntries )
  644   local result = {}
  645   for _, v in ipairs( searchEntries ) do
  646     local result_part = {}
  647     if v.objectName and v.objectName:len() > 0 then
  648       result_part.name = string.format("dn: %s", v.objectName)
  649     else
  650       result_part.name = "<ROOT>"
  651     end
  652 
  653     local attribs = {}
  654     if ( v.attributes ~= nil ) then
  655       for _, attrib in ipairs( v.attributes ) do
  656         for i=2, #attrib do
  657           -- do some additional Windows decoding
  658           if ( attrib[1] == "objectSid" ) then
  659             table.insert( attribs, string.format( "%s: %s", attrib[1], convertObjectSid( attrib[i] ) ) )
  660           elseif ( attrib[1] == "objectGUID") then
  661             local o = {string.unpack(("B"):rep(16), attrib[i] )}
  662             table.insert( attribs, string.format( "%s: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x%x",
  663               attrib[1], o[4], o[3], o[2], o[1], table.unpack(o, 5, 16)))
  664           elseif ( attrib[1] == "lastLogon" or attrib[1] == "lastLogonTimestamp" or attrib[1] == "pwdLastSet" or attrib[1] == "accountExpires" or attrib[1] == "badPasswordTime"  ) then
  665             table.insert( attribs, string.format( "%s: %s", attrib[1], convertADTimeStamp(attrib[i]) ) )
  666           elseif ( attrib[1] == "whenChanged" or attrib[1] == "whenCreated" or attrib[1] == "dSCorePropagationData" ) then
  667             table.insert( attribs, string.format( "%s: %s", attrib[1], convertZuluTimeStamp(attrib[i]) ) )
  668           else
  669             table.insert( attribs, string.format( "%s: %s", attrib[1], attrib[i] ) )
  670           end
  671         end
  672       end
  673       table.insert( result_part, attribs )
  674     end
  675     table.insert( result, result_part )
  676   end
  677   return result
  678 end
  679 
  680 --- Saves a search result as received from searchRequest to a file
  681 --
  682 -- Does some limited decoding of LDAP attributes
  683 --
  684 -- TODO: Add decoding of missing attributes
  685 -- TODO: Add decoding of userParameters
  686 -- TODO: Add decoding of loginHours
  687 --
  688 -- @param searchEntries table as returned from searchRequest
  689 -- @param filename the name of a save to save results to
  690 -- @return table suitable for <code>stdnse.format_output</code>
  691 function searchResultToFile( searchEntries, filename )
  692 
  693   local f = io.open( filename, "w")
  694 
  695   if ( not(f) ) then
  696     return false, ("ERROR: Failed to open file (%s)"):format(filename)
  697   end
  698 
  699   -- Build table structure.  Using a multi pass approach ( build table then populate table )
  700   -- because the objects returned may not necessarily have the same number of attributes
  701   -- making single pass CSV output generation problematic.
  702   -- Unfortunately the searchEntries table passed to this function is not organized in a
  703   -- way that make particular attributes for a given hostname directly addressable.
  704   --
  705   -- At some point restructuring the searchEntries table may be a good optimization target
  706 
  707   -- build table of attributes
  708   local attrib_table = {}
  709   for _, v in ipairs( searchEntries ) do
  710     if ( v.attributes ~= nil ) then
  711       for _, attrib in ipairs( v.attributes ) do
  712         for i=2, #attrib do
  713           if ( attrib_table[attrib[1]] == nil ) then
  714             attrib_table[attrib[1]] = ''
  715           end
  716         end
  717       end
  718     end
  719   end
  720 
  721   -- build table of hosts
  722   local host_table = {}
  723   for _, v in ipairs( searchEntries ) do
  724     if v.objectName and v.objectName:len() > 0 then
  725       local host = {}
  726 
  727       if v.objectName and v.objectName:len() > 0 then
  728         -- use a copy of the table here, assigning attrib_table into host_table
  729         -- links the values so setting it for one host changes the specific attribute
  730         -- values for all hosts.
  731         host_table[v.objectName] = {attributes = copyTable(attrib_table) }
  732       end
  733     end
  734   end
  735 
  736   -- populate the host table with values for each attribute that has valid data
  737   for _, v in ipairs( searchEntries ) do
  738     if ( v.attributes ~= nil ) then
  739       for _, attrib in ipairs( v.attributes ) do
  740         for i=2, #attrib do
  741           -- do some additional Windows decoding
  742           if ( attrib[1] == "objectSid" ) then
  743             host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = string.format( "%s", convertObjectSid(attrib[i]))
  744 
  745           elseif ( attrib[1] == "objectGUID") then
  746             local o = {string.unpack(("B"):rep(16), attrib[i] )}
  747             host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = string.format(
  748               "%s: %x%x%x%x-%x%x-%x%x-%x%x-%x%x%x%x%x%x",
  749               attrib[1], o[4], o[3], o[2], o[1], table.unpack(o, 5, 16))
  750 
  751           elseif ( attrib[1] == "lastLogon" or attrib[1] == "lastLogonTimestamp" or attrib[1] == "pwdLastSet" or attrib[1] == "accountExpires" or attrib[1] == "badPasswordTime" ) then
  752             host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = convertADTimeStamp(attrib[i])
  753 
  754           elseif ( attrib[1] == "whenChanged" or attrib[1] == "whenCreated" or attrib[1] == "dSCorePropagationData" ) then
  755             host_table[string.format("%s", v.objectName)].attributes[attrib[1]] = convertZuluTimeStamp(attrib[i])
  756 
  757           else
  758             host_table[v.objectName].attributes[attrib[1]] = string.format( "%s", attrib[i] )
  759           end
  760 
  761         end
  762       end
  763     end
  764   end
  765 
  766   -- write the new, fully populated table out to CSV
  767 
  768   -- initialize header row
  769   local output = "\"name\""
  770   for attribute, value in pairs(attrib_table) do
  771     output = output .. ",\"" .. attribute .. "\""
  772   end
  773   output = output .. "\n"
  774 
  775   -- gather host data from fields, add to output.
  776   for name, attribs in pairs(host_table) do
  777     output = output .. "\"" .. name .. "\""
  778     local host_attribs = attribs.attributes
  779     for attribute, value in pairs(attrib_table) do
  780       output = output .. ",\"" .. host_attribs[attribute] .. "\""
  781     end
  782     output = output .. "\n"
  783   end
  784 
  785   -- write the output to file
  786   if ( not(f:write( output .."\n" ) ) ) then
  787     f:close()
  788     return false, ("ERROR: Failed to write file (%s)"):format(filename)
  789   end
  790 
  791   f:close()
  792   return true
  793 end
  794 
  795 
  796 --- Extract naming context from a search response
  797 --
  798 -- @param searchEntries table containing searchEntries from a searchResponse
  799 -- @param attributeName string containing the attribute to extract
  800 -- @return table containing the attribute values
  801 function extractAttribute( searchEntries, attributeName )
  802   local attributeTbl = {}
  803   for _, v in ipairs( searchEntries ) do
  804     if ( v.attributes ~= nil ) then
  805       for _, attrib in ipairs( v.attributes ) do
  806         local attribType = attrib[1]
  807         for i=2, #attrib do
  808           if ( attribType:upper() == attributeName:upper() ) then
  809             table.insert( attributeTbl, attrib[i])
  810           end
  811         end
  812       end
  813     end
  814   end
  815   return ( #attributeTbl > 0 and attributeTbl or nil )
  816 end
  817 
  818 --- Convert Microsoft Active Directory timestamp format to a human readable form
  819 --  These values store time values in 100 nanoseconds segments from 01-Jan-1601
  820 --
  821 -- @param timestamp Microsoft Active Directory timestamp value
  822 -- @return string containing human readable form
  823 function convertADTimeStamp(timestamp)
  824 
  825   local result = 0
  826   -- Windows cannot represent this time, so we pre-calculated it:
  827   -- seconds since 1601/1/1 adjusted for local offset
  828   local base_time = -11644473600 - datetime.utc_offset()
  829 
  830   timestamp = tonumber(timestamp)
  831 
  832   if (timestamp and timestamp > 0) then
  833 
  834     -- The result value was 3036 seconds off what Microsoft says it should be.
  835     -- I have been unable to find an explanation for this, and have resorted to
  836     -- manually adjusting the formula.
  837 
  838     result = ( timestamp //  10000000 ) - 3036
  839     result = result + base_time
  840     result = datetime.format_timestamp(result, 0)
  841   else
  842     result = 'Never'
  843   end
  844 
  845   return result
  846 
  847 end
  848 
  849 --- Converts a non-delimited Zulu timestamp format to a human readable form
  850 --  For example 20110904003302.0Z becomes 2001/09/04 00:33:02 UTC
  851 --
  852 --
  853 -- @param timestamp in Zulu format without separators
  854 -- @return string containing human readable form
  855 function convertZuluTimeStamp(timestamp)
  856 
  857   if ( type(timestamp) == 'string' and string.sub(timestamp,-3) == '.0Z' ) then
  858 
  859     local year  = string.sub(timestamp,1,4)
  860     local month = string.sub(timestamp,5,6)
  861     local day   = string.sub(timestamp,7,8)
  862     local hour  = string.sub(timestamp,9,10)
  863     local mins  = string.sub(timestamp,11,12)
  864     local secs  = string.sub(timestamp,13,14)
  865     local result = year .. "/" .. month .. "/" .. day .. " " .. hour .. ":" .. mins .. ":" .. secs .. " UTC"
  866 
  867     return result
  868 
  869   else
  870     return 'Invalid date format'
  871   end
  872 
  873 end
  874 
  875 --- Converts the objectSid Active Directory attribute
  876 --  from hex to a human readable string
  877 --
  878 --  Example: 1-5-21-542885397-2936741293-3965599772-500
  879 --
  880 -- @param hex string form of objectSid from LDAP response
  881 -- @return string containing human readable form
  882 function convertObjectSid(data)
  883 
  884   local pos, revision, auth, sub_auth_size, sub_auth, result
  885 
  886   revision, pos = string.unpack('I1', data, 1)
  887   sub_auth_size, pos = string.unpack('I1', data, pos)
  888   auth, pos = string.unpack('>I6', data, pos)
  889 
  890   sub_auth = ''
  891   local tmp
  892   local cnt = 0
  893   while (cnt < sub_auth_size) do
  894     tmp, pos = string.unpack('<I4', data, pos)
  895     sub_auth = sub_auth .. '-' .. tmp
  896     cnt = cnt + 1
  897   end
  898 
  899   result = revision .. '-' .. auth .. sub_auth
  900   return result
  901 
  902 end
  903 
  904 --- Creates a copy of a table
  905 --
  906 --
  907 -- @param targetTable  table object to copy
  908 -- @return table object containing copy of original
  909 function copyTable(targetTable)
  910   local temp = { }
  911   for key, val in pairs(targetTable) do
  912     temp[key] = val
  913   end
  914   return setmetatable(temp, getmetatable(targetTable))
  915 end
  916 
  917 return _ENV;