1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00

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
This commit is contained in:
nnposter
2019-12-19 20:13:16 +00:00
parent 1d8ed90a5c
commit 6998bfca49
3 changed files with 226 additions and 157 deletions

View File

@@ -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]

View File

@@ -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("<I2 I2 I2 I2 BBI2 I4 I2 I2 I2 I2 I2 BBI2 ",
parameter_size, -- Total parameter count.
data_size, -- Total data count.
0x000a, -- Max parameter count.
0x3984, -- Max data count.
0x00, -- Max setup count.
0x00, -- Reserved.
0x0000, -- Flags (0x0000 = 2-way transaction, don't disconnect TIDs).
0x00001388, -- Timeout (0x00000000 = return immediately).
0x0000, -- Reserved.
parameter_size, -- Parameter bytes.
parameter_offset, -- Parameter offset.
data_size, -- Data bytes.
data_offset, -- Data offset.
0x01, -- Setup Count
0x00, -- Reserved
sub_command -- Sub command
)
-- SMB parameters are 31 bytes long, incl. initial WordCount field
-- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/f7d148cd-e3d5-49ae-8b37-9633822bfeac
local parameters = string.pack("<I2 I2 I2 I2 BB I2 I4 I2 I2 I2 I2 I2 BB I2 ",
trans2_param_len, -- Total parameter count
trans2_data_len, -- Total data count
0x000a, -- Max parameter count
0xff80, -- Max data count
0x00, -- Max setup count
0x00, -- Reserved
0x0000, -- Flags (2-way transaction, don't disconnect TIDs)
5000, -- Timeout (ms)
0x0000, -- Reserved
trans2_param_len, -- Parameter count
trans2_param_pos, -- Parameter offset
trans2_data_len, -- Data count
trans2_data_pos, -- Data offset
0x01, -- Setup count
0x00, -- Reserved
sub_command -- Sub command
)
local data = "\0\0\0" .. (function_parameters or '')
.. (function_data or '')
local data = pad1 .. trans2_param .. pad2 .. trans2_data
-- Send the transaction request
stdnse.debug2("SMB: Sending SMB_COM_TRANSACTION2")
local result, err = smb_send(smb, header, parameters, data, overrides)
if(result == false) then
return false, err
end
return true
return smb_send(smb, header, parameters, data, overrides)
end
local function receive_transaction2(smb)
@@ -2727,12 +2723,99 @@ function file_delete(host, share, remotefile)
return true
end
-- Sends TRANS2_FIND_FIRST2 / TRANS2_FIND_NEXT2 request, takes care of
-- short/fragmented responses, and returns a list of file entries
--
-- @param smbstate the SMB object associated with the connection
-- @param srch_id of search to resume (for TRANS2_FIND_NEXT2) or nil
-- @param trans2_params string representing Trans2_Parameters
-- @return status of the request
-- @return srch_id of search to resume later, or nil if the search completed
-- or the error message if status is false
-- @return list of file entries
local function send_and_receive_find_request(smbstate, srch_id, trans2_params)
local TRANS2_FIND_FIRST2 = 1
local TRANS2_FIND_NEXT2 = 2
local sub_command = srch_id and TRANS2_FIND_NEXT2 or TRANS2_FIND_FIRST2
local status = send_transaction2(smbstate, sub_command, trans2_params, "")
if not status then
return false, "Failed to send data to server: send_transaction2"
end
local resp
status, resp = receive_transaction2(smbstate)
if not status or #resp.parameters < 2 then
return false, "Failed to receive data from server: receive_transaction2"
end
local param_pos = 1
if sub_command == TRANS2_FIND_FIRST2 then
srch_id, param_pos = string.unpack("<I2", resp.parameters, param_pos)
end
-- parse Trans2_Parameters
-- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/4e65d94e-09af-4511-a77a-b73adf1c52d6
local param_fmt = "<I2 I2 xx I2"
if #resp.parameters < param_pos - 1 + param_fmt:packsize() then
return false, "Truncated response from server: receive_transaction2"
end
local srch_cnt, srch_end, last_name_pos = param_fmt:unpack(resp.parameters, param_pos)
-- format of SMB_FIND_FILE_BOTH_DIRECTORY_INFO, without trailing FileName
-- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/2aa849f4-1bc0-42bf-9c8f-d09f11fccc4c
local entry_fmt = "<I4 xxxx I8 I8 I8 I8 I8 I8 I4 I4 xxxx B x c24"
local entry_len = entry_fmt:packsize()
-- check if we need more packets to reassemble this transaction
while #resp.data < last_name_pos + entry_len do
local status, tmp = receive_transaction2(smbstate)
if not status then
return false, "Truncated response from receive_transaction2"
end
resp.data = resp.data .. tmp.data
end
-- parse response, based on SMB_FIND_FILE_BOTH_DIRECTORY_INFO
local entries = {}
local data_pos = 1
while srch_cnt > 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
-- <code>maxfiles</code> how many files to request in a single Trans2 op
-- <code>srch_attrs</code> table containing one or more of the following boolean attributes:
-- <code>ro</code> - find read only files
-- <code>hidden</code> - find hidden files
@@ -2741,126 +2824,91 @@ end
-- <code>dir</code> - find directories
-- <code>archive</code> - 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("<I2", response.parameters, pos)
-- check if we need more packets to reassemble this transaction
local NE_UP_TO_FNAME_SIZE = 94
while ( last_name_offset > ( #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("<I2 I2 I2 I2 I4",
nattrs, srch_count, flags, loi, storage_type) .. fname .. string.rep('\0', (4 - pad) % 4)
local function next_item()
local status, response = send_and_receive_find_request(smbstate, TRANS2_FIND_FIRST2, function_parameters)
if not status or #response.parameters < 4 then
return
end
local srch_id = string.unpack("<I2", response.parameters)
local stop_loop = ( string.unpack("<I2", response.parameters, 5) ~= 0 )
local first = true
local last_name
repeat
local pos = 1
if ( not(first) ) then
local function_parameters = string.pack("<I2 I2 I2 I4 I2 z", srch_id, srch_count, loi, 0, flags, last_name)
status, response = send_and_receive_find_request(smbstate, TRANS2_FIND_NEXT2, function_parameters)
if not status or #response.parameters < 2 then
local function next_entry()
if entry_idx > #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 I2 I2 I2 I4 z",
srch_attrs_mask, -- what types of files to return
max_srch_cnt, -- maximum number of returned entries
srch_flags, -- Flags
srch_info_lvl, -- level of returned file details
0, -- SearchStorageType
fname) -- file name to search for
-- FIXME filename ASCII vs UNICODE
else -- TRANS2_FIND_NEXT2
if not srch_id then -- the search is over
return
end
-- check whether END-OF-SEARCH was set
stop_loop = ( string.unpack(">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("<I2 I2 I2 I4 I2 z",
srch_id, -- which search to resume
max_srch_cnt, -- maximum number of returned entries
srch_info_lvl, -- level of returned file details
0, -- ResumeKey
srch_flags, -- Flags
last_fname) -- last file name previously returned
-- FIXME wtf is ResumeKey?
end
-- parse response, based on LOI == 260
local fe_format = "<I4 I4 I8 I8 I8 I8 I8 I8 I4 I4 I4 Bx c24"
while #response.data - pos + 1 >= 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 =
{

View File

@@ -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