From 727661921a10fa516db74bb8272ac46c530f90e7 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 3 Oct 2010 22:40:41 +0000 Subject: [PATCH] Implemented some changes (mostly in error handling) to let smb scripts run in parallel with each other against a single host. Previously, I had used a mutex to prevent that from happening. The changes are summarized in the following post: http://seclists.org/nmap-dev/2010/q3/819, but it comes down to three things: 1) Set the 'vc' ('virtual circuit') id to a non-zero value (if it's 0, the smb server will disconnect all hosts who are already connected) 2) Handle the authentication error NT_STATUS_REQUEST_NOT_ACCEPTED, which indicates that there are too many connected hosts (11 is the default on most versions of Windows, or 10 for the anonymous account). If we see the error, we wait and try again. 3) Handle the file creation error, NT_STATUS_PIPE_NOT_AVAILABLE, which appears to be caused by a race condition of some sort. It happens when a large number of connections are attempted simultaneously, and is fixed by a short backoff (50ms worked fine, but I'm using 100ms) The end result is a significant speedup in our SMB checks without losing data. --- nselib/smb.lua | 270 ++++++++++++++++++++++--------------------------- 1 file changed, 121 insertions(+), 149 deletions(-) diff --git a/nselib/smb.lua b/nselib/smb.lua index 4dc21c770..553722891 100644 --- a/nselib/smb.lua +++ b/nselib/smb.lua @@ -132,9 +132,6 @@ command_names = {} status_codes = {} status_names = {} -local mutexes = setmetatable({}, {__mode = "k"}); ---local debug_mutex = nmap.mutex("SMB-DEBUG") - local TIMEOUT = 10000 ---Wrapper around smbauth.add_account. @@ -174,68 +171,6 @@ function get_overrides_anonymous(overrides) end end - ----Returns the mutex that should be used by the current connection. This mutex attempts --- to use the name, first, then falls back to the IP if no name was returned. --- ---@param smbstate The SMB object associated with the connection ---@return A mutex -local function get_mutex(smbstate) - local mutex_name = "SMB-" - local mutex - --- if(nmap.debugging() > 0) then --- return debug_mutex --- end - - -- Decide whether to use the name or the ip address as the unique identifier - if(smbstate['name'] ~= nil) then - mutex_name = mutex_name .. smbstate['name'] - else - mutex_name = mutex_name .. smbstate['ip'] - end - - if(mutexes[smbstate] == nil) then - mutex = nmap.mutex(mutex_name) - mutexes[smbstate] = mutex - else - mutex = mutexes[smbstate] - end - - stdnse.print_debug(3, "SMB: Using mutex named '%s'", mutex_name) - - return mutex -end - ----Locks the mutex being used by this host. Doesn't return until it successfully --- obtains a lock. --- ---@param smbstate The SMB object associated with the connection ---@param func A name to associate with this call (used purely for debugging --- and logging) -local function lock_mutex(smbstate, func) - local mutex - - stdnse.print_debug(3, "SMB: Attempting to lock mutex [%s]", func) - mutex = get_mutex(smbstate) - mutex "lock" - stdnse.print_debug(3, "SMB: Mutex lock obtained [%s]", func) -end - ----Unlocks the mutex being used by this host. --- ---@param smbstate The SMB object associated with the connection ---@param func A name to associate with this call (used purely for debugging --- and logging) -local function unlock_mutex(smbstate, func) - local mutex - - stdnse.print_debug(3, "SMB: Attempting to release mutex [%s]", func) - mutex = get_mutex(smbstate) - mutex "done" - stdnse.print_debug(3, "SMB: Mutex released [%s]", func) -end - ---Convert a status number from the SMB header into a status name, returning an error message (not nil) if -- it wasn't found. -- @@ -304,9 +239,7 @@ function disable_extended(smb) smb['extended_security'] = false end ---- Begins a SMB session, automatically determining the best way to connect. Also starts a mutex --- with mutex_id. This prevents multiple threads from making queries at the same time (which breaks --- SMB). +--- Begins a SMB session, automatically determining the best way to connect. -- -- @param host The host object -- @return (status, smb) if the status is true, result is the newly crated smb object; @@ -318,6 +251,8 @@ function start(host) state['uid'] = 0 state['tid'] = 0 + state['mid'] = 1 + state['pid'] = math.random(32766) + 1 state['host'] = host state['ip'] = host.ip state['sequence'] = -1 @@ -341,17 +276,14 @@ function start(host) return false, "SMB: Couldn't find a valid port to check" end - -- Initialize the accounts for logging on (note: this has to be outside the mutex, or things break) + -- Initialize the accounts for logging on smbauth.init_account(host) - lock_mutex(state, "start(1)") - if(port ~= 139) then status, state['socket'] = start_raw(host, port) state['port'] = port if(status == false) then - unlock_mutex(state, "start(1)") return false, state['socket'] end return true, state @@ -360,15 +292,12 @@ function start(host) status, state['socket'] = start_netbios(host, port) state['port'] = port if(status == false) then - unlock_mutex(state, "start(2)") return false, state['socket'] end return true, state end - unlock_mutex(state, "start(3)") - return false, "SMB: Couldn't find a valid port to check" end @@ -447,8 +376,7 @@ function start_ex(host, negotiate_protocol, start_session, tree_connect, create_ return true, smbstate end ---- Kills the SMB connection, closes the socket, and releases the mutex. Because of the mutex --- being released, a script HAS to call stop before it exits, no matter why it's exiting! +--- Kills the SMB connection and closes the socket. -- -- In addition to killing the connection, this function will log off the user and disconnect -- the connected tree, if possible. @@ -466,8 +394,6 @@ function stop(smb) logoff(smb) end - unlock_mutex(smb, "stop()") - stdnse.print_debug(2, "SMB: Closing socket") if(smb['socket'] ~= nil) then local status, err = smb['socket']:close() @@ -721,9 +647,9 @@ local function smb_encode_header(smb, command, overrides) (overrides['signature'] or 0), -- extra (signature) (overrides['extra'] or 0), -- extra (unused) (overrides['tid'] or smb['tid']), -- tid - (overrides['pid'] or 12345), -- pid + (overrides['pid'] or smb['pid']), -- pid (overrides['uid'] or smb['uid']), -- uid - (overrides['uid'] or 0) -- mid + (overrides['mid'] or smb['mid']) -- mid ) return header @@ -1152,7 +1078,7 @@ function negotiate_protocol(smb, overrides) end -function start_session_basic(smb, log_errors, overrides) +local function start_session_basic(smb, log_errors, overrides) local i, err local status, result local header, parameters, data, domain @@ -1161,6 +1087,7 @@ function start_session_basic(smb, log_errors, overrides) local andx_command, andx_reserved, andx_offset, action local os, lanmanager local username, domain, password, password_hash, hash_type + local busy_count = 0 header = smb_encode_header(smb, command_codes['SMB_COM_SESSION_SETUP_ANDX'], overrides) @@ -1188,7 +1115,7 @@ function start_session_basic(smb, log_errors, overrides) 0x0000, -- ANDX -- next offset 0xFFFF, -- Max buffer size 0x0001, -- Max multiplexes - 0x0000, -- Virtual circuit num + 0x0001, -- Virtual circuit num smb['session_key'], -- The session key #lanman, -- ANSI/Lanman password length #ntlm, -- Unicode/NTLM password length @@ -1269,17 +1196,30 @@ function start_session_basic(smb, log_errors, overrides) return true else - -- This username failed, print a warning and keep going - if(log_errors == nil or log_errors == true) then - stdnse.print_debug(1, "SMB: Login as %s\\%s failed (%s)", domain, stdnse.string_or_blank(username), get_status_name(status)) - end + -- Check if we got the error NT_STATUS_REQUEST_NOT_ACCEPTED + if(status == 0xc00000d0) then + busy_count = busy_count + 1 - -- Go to the next account - if(overrides == nil or overrides['username'] == nil) then - smbauth.next_account(smb['host']) - result, username, domain, password, password_hash, hash_type = smbauth.get_account(smb['host']) + if(busy_count > 9) then + return false, "SMB: ERROR: Server has too many active connections; giving up." + end + + local backoff = math.random() * 10 + stdnse.print_debug(1, "SMB: Server has too many active connections; pausing for %s seconds.", math.floor(backoff * 100) / 100) + stdnse.sleep(backoff) else - result = false + -- This username failed, print a warning and keep going + if(log_errors == nil or log_errors == true) then + stdnse.print_debug(1, "SMB: Login as %s\\%s failed (%s)", domain, stdnse.string_or_blank(username), get_status_name(status)) + end + + -- Go to the next account + if(overrides == nil or overrides['username'] == nil) then + smbauth.next_account(smb['host']) + result, username, domain, password, password_hash, hash_type = smbauth.get_account(smb['host']) + else + result = false + end end end end @@ -1291,7 +1231,7 @@ function start_session_basic(smb, log_errors, overrides) return false, username end -function start_session_extended(smb, log_errors, overrides) +local function start_session_extended(smb, log_errors, overrides) local i local status, status_name, result, err local header, parameters, data @@ -1300,6 +1240,7 @@ function start_session_extended(smb, log_errors, overrides) local andx_command, andx_reserved, andx_offset, action, security_blob_length local os, lanmanager local username, domain, password, password_hash, hash_type + local busy_count = 0 -- Set a default status_name, in case everything fails status_name = "An unknown error has occurred" @@ -1342,7 +1283,7 @@ function start_session_extended(smb, log_errors, overrides) 0x0000, -- ANDX -- next offset 0xFFFF, -- Max buffer size 0x0001, -- Max multiplexes - 0x0000, -- Virtual circuit num + 0x0001, -- Virtual circuit num smb['session_key'], -- The session key #security_blob, -- Security blob length 0x00000000, -- Reserved @@ -1423,24 +1364,38 @@ function start_session_extended(smb, log_errors, overrides) end -- Should we parse the parameters/data? until status_name ~= "NT_STATUS_MORE_PROCESSING_REQUIRED" - -- Display a message to the user, and try the next account - if(log_errors == nil or log_errors == true) then - stdnse.print_debug(1, "SMB: Extended login as %s\\%s failed (%s)", domain, stdnse.string_or_blank(username), status_name) + -- Check if we got the error NT_STATUS_REQUEST_NOT_ACCEPTED + if(status == 0xc00000d0) then + busy_count = busy_count + 1 + + if(busy_count > 9) then + return false, "SMB: ERROR: Server has too many active connections; giving up." + end + + local backoff = math.random() * 10 + stdnse.print_debug(1, "SMB: Server has too many active connections; pausing for %s seconds.", math.floor(backoff * 100) / 100) + stdnse.sleep(backoff) + else + -- Display a message to the user, and try the next account + if(log_errors == nil or log_errors == true) then + stdnse.print_debug(1, "SMB: Extended login as %s\\%s failed (%s)", domain, stdnse.string_or_blank(username), status_name) + end + + -- Go to the next account + if(overrides == nil or overrides['username'] == nil) then + smbauth.next_account(smb['host']) + result, username, domain, password, password_hash, hash_type = smbauth.get_account(smb['host']) + if(not(result)) then + return false, username + end + else + result = false + end end - -- Reset the user id and security_blob + -- Reset the user id smb['uid'] = 0 - -- Go to the next account - if(overrides == nil or overrides['username'] == nil) then - smbauth.next_account(smb['host']) - result, username, domain, password, password_hash, hash_type = smbauth.get_account(smb['host']) - if(not(result)) then - return false, username - end - else - result = false - end end -- Loop over the accounts if(log_errors == nil or log_errors == true) then @@ -1666,50 +1621,67 @@ function create_file(smb, path, overrides) local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid local andx_command, andx_reserved, andx_offset local oplock_level, fid, create_action, created, last_access, last_write, last_change, attributes, allocation_size, end_of_file, filetype, ipc_state, is_directory + local error_count = 0 - -- Make sure we have overrides - overrides = overrides or {} + repeat + local mutex = nmap.mutex(smb['host']) + mutex "lock" + + -- Make sure we have overrides + overrides = overrides or {} + + header = smb_encode_header(smb, command_codes['SMB_COM_NT_CREATE_ANDX'], overrides) + parameters = bin.pack(" 10) then + return false, "SMB: ERROR: Server returned NT_STATUS_PIPE_NOT_AVAILABLE too many times; giving up." + end + stdnse.print_debug(1, "WARNING: Server refused connection with NT_STATUS_PIPE_NOT_AVAILABLE; trying again") + stdnse.sleep(.2) + end + until (status ~= 0xc00000ac) - data = bin.pack("z", path) - - -- Send the create file - stdnse.print_debug(2, "SMB: Sending SMB_COM_NT_CREATE_ANDX") - local result, err = smb_send(smb, header, parameters, data, overrides) - if(result == false) then - return false, err - end - - -- Read the result - status, header, parameters, data = smb_read(smb, false) - if(status ~= true) then - return false, header - end - - -- Check if we were allowed in - local uid, tid - pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, uid, mid = bin.unpack("