From 6998bfca49b3dc7f3df42369e2307143eeee60fa Mon Sep 17 00:00:00 2001 From: nnposter Date: Thu, 19 Dec 2019 20:13:16 +0000 Subject: [PATCH] Refactors function smb.find_files() - Replaces its coroutine design to avoid sharing sockets across threads (Fixes #1837) - Corrects conversion of file attributes into bitmask - Removes side effect of modifying parameter "options" by populating member "srch_attrs" - Implements options.maxfiles to take advantage of script arg ls.maxfiles, reducing file requests that would be ultimately ignored anyway - Improves performace by supporting larger SMB block sizes - Implements rudimentary support for Trans2_Data by smb.send_transaction2() - Adds standard definitions for SMB file attributes --- CHANGELOG | 3 + nselib/smb.lua | 378 ++++++++++++++++++++++++++------------------- scripts/smb-ls.nse | 2 +- 3 files changed, 226 insertions(+), 157 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4ad690411..5d35fed93 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,9 @@ o [Windows] Add support for the new loopback behavior in Npcap 0.9983. This Adapter to be installed, which was a source of problems for some users. [Daniel Miller] +o [NSE][GH1837] Nmap no longer crashes when SMB scripts, such as smb-ls, call + smb.find_files [nnposter] + o [NSE][GH1802] The MongoDB library was causing errors when assembling protocol payloads. [nnposter] diff --git a/nselib/smb.lua b/nselib/smb.lua index c31b1c760..3931777cd 100644 --- a/nselib/smb.lua +++ b/nselib/smb.lua @@ -122,7 +122,6 @@ -- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html ----------------------------------------------------------------------- local asn1 = require "asn1" -local coroutine = require "coroutine" local datetime = require "datetime" local io = require "io" local math = require "math" @@ -2132,64 +2131,61 @@ end -- Implements SMB_COM_TRANSACTION2 to support the find_files function -- This function has not been extensively tested -- ---@param smb The SMB object associated with the connection ---@param sub_command The SMB_COM_TRANSACTION2 sub command ---@param function_parameters The parameter data to pass to the function. This is untested, since none of the --- transactions I've done have required parameters. ---@param function_data The data to send with the packet. This is basically the next protocol layer ---@param overrides The overrides table ---@return (status, result) If status is false, result is an error message. Otherwise, result is a table --- containing 'parameters' and 'data', representing the parameters and data returned by the server. -local function send_transaction2(smb, sub_command, function_parameters, function_data, overrides) +--@param smb SMB object associated with the connection +--@param sub_command code of a SMB_COM_TRANSACTION2 sub command +--@param trans2_param Parameter data to pass to the function +--@param trans2_data Data to send with the packet +--@param overrides The overrides table +--@return status Boolean outcome of the request +--@return error error message if the status is false +local function send_transaction2(smb, sub_command, trans2_param, trans2_data, overrides) overrides = overrides or {} - local header, parameters, data - local parameter_offset = 0 - local parameter_size = 0 - local data_offset = 0 - local data_size = 0 - local total_word_count, total_data_count, reserved1, parameter_count, parameter_displacement, data_count, data_displacement, setup_count, reserved2 - local response = {} + trans2_param = trans2_param or "" + trans2_data = trans2_data or "" - -- Header is 0x20 bytes long (not counting NetBIOS header). - header = smb_encode_header(smb, command_codes['SMB_COM_TRANSACTION2'], overrides) -- 0x32 = SMB_COM_TRANSACTION2 + local header = smb_encode_header(smb, command_codes['SMB_COM_TRANSACTION2'], overrides) + local pad1 = "\0\0\0" -- Name, Pad1 + local pad2 = ("\0"):rep((4 - #trans2_param % 4) % 4) - if(function_parameters) then - parameter_offset = 0x44 - parameter_size = #function_parameters - data_offset = #function_parameters + 33 + 32 + local trans2_param_len = #trans2_param + -- 68 = 32 SMB header + -- + 31 SMB parameters + -- + 2 SMB data ByteCount field + -- + 3 #pad1 + local trans2_param_pos = 68 + local trans2_data_len = #trans2_data + local trans2_data_pos = trans2_param_pos + trans2_param_len + #pad2 + if trans2_data_len == 0 then + pad2 = "" + trans2_data_pos = 0 end - -- Parameters are 0x20 bytes long. - parameters = string.pack(" 0 do + if #resp.data - data_pos + 1 < entry_len then + return false, "Truncated response from receive_transaction2" + end + local entry = {} + local next_pos, fn_pos, fn_len, sfn_len + next_pos, entry.created, entry.accessed, entry.write, entry.change, + entry.eof, entry.alloc_size, entry.attrs, fn_len, sfn_len, + entry.s_fname, fn_pos = entry_fmt:unpack(resp.data, data_pos) + + local time = entry.created + time = (time // 10000000) - 11644473600 + entry.created = datetime.format_timestamp(time) + + if sfn_len > 0 then + entry.s_fname = entry.s_fname:sub(1, sfn_len) + else + entry.s_fname = nil + end + + if #resp.data - fn_pos + 1 < fn_len then + return false, "Truncated response from receive_transaction2" + end + entry.fname = string.unpack("z", resp.data, fn_pos) + table.insert(entries, entry) + data_pos = data_pos + next_pos + srch_cnt = srch_cnt - 1 + end + return true, (srch_end == 0 and srch_id or nil), entries +end + --- -- List files based on a pattern within a given share and directory -- -- @param smbstate the SMB object associated with the connection -- @param fname filename to search for, relative to share path -- @param options table containing none or more of the following +-- maxfiles how many files to request in a single Trans2 op -- srch_attrs table containing one or more of the following boolean attributes: -- ro - find read only files -- hidden - find hidden files @@ -2741,126 +2824,91 @@ end -- dir - find directories -- archive - find archived files -- @return iterator function retrieving the next result -function find_files(smbstate, fname, options) - local TRANS2_FIND_FIRST2, TRANS2_FIND_NEXT2 = 1, 2 +function find_files (smbstate, fname, options) options = options or {} - if (not(options.srch_attrs)) then - options.srch_attrs = { ro = true, hidden = true, system = true, dir = true} + -- convert options.srch_attrs to a bitmap + local xlat_srch_attrs = {ro = "SMB_FILE_ATTRIBUTE_READONLY", + hidden = "SMB_FILE_ATTRIBUTE_HIDDEN", + system = "SMB_FILE_ATTRIBUTE_SYSTEM", + volid = "SMB_FILE_ATTRIBUTE_VOLUME", + dir = "SMB_FILE_ATTRIBUTE_DIRECTORY", + archive = "SMB_FILE_ATTRIBUTE_ARCHIVE"} + local srch_attrs_mask = 0 + local srch_attrs = options.srch_attrs or {ro=true, hidden=false, system=true, dir=true} + for k, v in pairs(srch_attrs) do + if v then + srch_attrs_mask = srch_attrs_mask | file_attributes[xlat_srch_attrs[k]] + end end - local nattrs = (( options.srch_attrs.ro and 1 or 0 ) + ( options.srch_attrs.hidden and 2 or 0 ) + - ( options.srch_attrs.hidden and 2 or 0 ) + ( options.srch_attrs.system and 4 or 0 ) + - ( options.srch_attrs.volid and 8 or 0 ) + ( options.srch_attrs.dir and 16 or 0 ) + - ( options.srch_attrs.archive and 32 or 0 )) - - if ( not(fname) ) then - fname = '\\*\0' - elseif( fname:sub(1,1) ~= '\\' ) then - fname = '\\' .. fname .. '\0' + fname = fname or '\\*' + if fname:sub(1,1) ~= '\\' then + fname = '\\' .. fname end - -- Sends the request and takes care of short/fragmented responses - local function send_and_receive_find_request(smbstate, trans_type, function_parameters) - - local status, err = send_transaction2(smbstate, trans_type, function_parameters, "") - if ( not(status) ) then - return false, "Failed to send data to server: send_transaction2" - end - - local status, response = receive_transaction2(smbstate) - if not status or #response.parameters < 2 then - return false, "Failed to receive data from server: receive_transaction2" - end - - local pos = ( TRANS2_FIND_FIRST2 == trans_type and 9 or 7 ) - local last_name_offset = string.unpack(" ( #response.data - NE_UP_TO_FNAME_SIZE ) ) do - local status, tmp = receive_transaction2(smbstate) - if ( not(status) ) then - return false, "Failed to receive data from receive_transaction2" - end - response.data = response.data .. tmp.data - end - - return true, response + local srch_flags = 0x0002 | 0x0004 -- SMB_FIND_CLOSE_AT_EOS, SMB_FIND_RETURN_RESUME_KEYS + local srch_info_lvl = 0x0104 -- SMB_FIND_FILE_BOTH_DIRECTORY_INFO + local max_srch_cnt = tonumber(options.maxfiles) + if max_srch_cnt and max_srch_cnt > 0 then + max_srch_cnt = math.floor(4 + math.min(1020, max_srch_cnt)) + else + max_srch_cnt = 1024 end - local srch_count = 173 -- picked up by wireshark - local flags = 6 -- Return RESUME keys, close search if END OF SEARCH is reached - local loi = 260 -- Level of interest, return SMB_FIND_FILE_BOTH_DIRECTORY_INFO - local storage_type = 0 -- despite the documentation of having to be either 0x01 or 0x40, wireshark reports 0 + -- state variables for next_entry() iterator + local first_run = true + local srch_id = nil + local last_fname = nil + local entries = {} + local entry_idx = 1 - -- SMB header: 32 - -- trans2 header: 36 - -- FIND_FIRST2 parameters: 12 + #fname - local pad = ( 32 + 36 + 12 + #fname ) % 4 - local function_parameters = string.pack(" #entries then -- get more file entries from the target + local trans2_params + if first_run then -- TRANS2_FIND_FIRST2 + first_run = false + -- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/b2b2a730-9499-4f05-884e-d5bb7b9caf90 + trans2_params = string.pack("I2", response.parameters, 3) ~= 0 ) + -- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/80dc980e-fe03-455c-ada6-7c5dd6c551ba + trans2_params = string.pack("= string.packsize(fe_format) do - local fe = {} - local last_pos = pos - local ne, f_len, ea_len, sf_len - ne, fe.fi, fe.created, fe.accessed, fe.write, fe.change, - fe.eof, fe.alloc_size, fe.attrs, f_len, ea_len, sf_len, fe.s_fname, pos = string.unpack(fe_format, response.data, pos) - - local time = fe.created - time = (time // 10000000) - 11644473600 - fe.created = datetime.format_timestamp(time) - - -- TODO: cleanup fe.s_fname - if #response.data - pos + 1 < f_len then - break - end - fe.fname, pos = string.unpack("c" .. f_len, response.data, pos) - pos = last_pos + ne - - -- removing trailing zero bytes from file name - fe.fname = fe.fname:sub(1, -2) - last_name = fe.fname - - coroutine.yield(fe) - if ne == 0 then - break - end + local status + status, srch_id, entries = send_and_receive_find_request(smbstate, srch_id, trans2_params) + if not status then + stdnse.debug1("Routine find_files failed with error: %s", srch_id) + srch_id = nil + entries = {} end - first = false - until(stop_loop) - return + entry_idx = 1 + if #entries == 0 then + return + end + end + local entry = entries[entry_idx] + last_fname = entry.fname + entry_idx = entry_idx + 1 + return entry end - return coroutine.wrap(next_item) + return next_entry end ---Determine whether or not the anonymous user has write access on the share. This is done by creating then @@ -3641,6 +3689,24 @@ for i, v in pairs(command_codes) do end +-- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/2198f480-e047-4df0-ba64-f28eadef00b9 +file_attributes = +{ + SMB_FILE_ATTRIBUTE_NORMAL = 0x0000, + SMB_FILE_ATTRIBUTE_READONLY = 0x0001, + SMB_FILE_ATTRIBUTE_HIDDEN = 0x0002, + SMB_FILE_ATTRIBUTE_SYSTEM = 0x0004, + SMB_FILE_ATTRIBUTE_VOLUME = 0x0008, + SMB_FILE_ATTRIBUTE_DIRECTORY = 0x0010, + SMB_FILE_ATTRIBUTE_ARCHIVE = 0x0020, + SMB_SEARCH_ATTRIBUTE_READONLY = 0x0100, + SMB_SEARCH_ATTRIBUTE_HIDDEN = 0x0200, + SMB_SEARCH_ATTRIBUTE_SYSTEM = 0x0400, + SMB_SEARCH_ATTRIBUTE_DIRECTORY = 0x1000, + SMB_SEARCH_ATTRIBUTE_ARCHIVE = 0x2000 +} + + -- see http://msdn.microsoft.com/en-us/library/cc231196(v=prot.10).aspx status_codes = { diff --git a/scripts/smb-ls.nse b/scripts/smb-ls.nse index 09570aa6e..a6fdfa884 100644 --- a/scripts/smb-ls.nse +++ b/scripts/smb-ls.nse @@ -193,7 +193,7 @@ action = function(host) -- remove leading slash arg_path = ( arg_path:sub(1,2) == '\\' and arg_path:sub(2) or arg_path ) - local options = {} + local options = {maxfiles = ls.config('maxfiles')} local depth, path, dirs = 0, arg_path, {} local file_count, dir_count, total_bytes = 0, 0, 0 local continue = true