--- -- Implements functionality related to Server Message Block (SMB, an extension -- of CIFS) version 2 traffic, which is a Windows protocol. -- -- SMB traffic is normally sent to/from ports 139 or 445 of Windows systems. Microsoft's -- extensive documentation is available at the following URL: -- * SMB2: https://msdn.microsoft.com/en-us/library/cc246482.aspx ----------------------------------------------------------------------- local asn1 = require "asn1" local bin = require "bin" local bit = require "bit" local coroutine = require "coroutine" local io = require "io" local math = require "math" local match = require "match" local netbios = require "netbios" local nmap = require "nmap" local os = require "os" local smbauth = require "smbauth" local stdnse = require "stdnse" local string = require "string" local table = require "table" local unicode = require "unicode" local smb = require "smb" _ENV = stdnse.module("smb2", stdnse.seeall) -- These arrays are filled in with constants at the bottom of this file command_codes = {} command_names = {} status_codes = {} status_names = {} filetype_codes = {} filetype_names = {} file_attributes = {} smb2_flags = {} smb2_OplockLevel = {} smb2_ImpersonationLevel = {} file_share_access = {} create_disposition = {} create_options = {} local TIMEOUT = 10000 ---Wrapper around smbauth.add_account. function add_account(host, username, domain, password, password_hash, hash_type, is_admin) smbauth.add_account(host, username, domain, password, password_hash, hash_type, is_admin) end ---Wrapper around smbauth.get_account. function get_account(host) return smbauth.get_account(host) end ---Get an 'overrides' table for the anonymous user -- --@param overrides [optional] A base table of overrides. The appropriate fields will be added. function get_overrides_anonymous(overrides) if(not(overrides)) then return {username='', domain='', password='', password_hash=nil, hash_type='none'} else overrides['username'] = '' overrides['domain'] = '' overrides['password'] = '' overrides['password_hash'] = '' overrides['hash_type'] = 'none' end end ---Convert a status number from the SMB header into a status name, returning an error message (not nil) if -- it wasn't found. -- --@param status The numerical status. --@return A string representing the error. Never nil. function get_status_name(status) if(status_names[status] == nil) then -- If the name wasn't found in the array, do a linear search on it for i, v in pairs(status_names) do if(v == status) then return i end end return string.format("NT_STATUS_UNKNOWN (0x%08x)", status) else return status_names[status] end end --- 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; -- otherwise, socket is the error message. function start(host) local port = smb.get_port(host) local status, result local state = {} 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 state['MessageId'] = -1 -- Store the name of the server local nbcache_mutex = nmap.mutex("Netbios lookup mutex") nbcache_mutex "lock" if ( not(host.registry['netbios_name']) ) then status, result = netbios.get_server_name(host.ip) if(status == true) then host.registry['netbios_name'] = result state['name'] = result end else stdnse.debug2("SMB: Resolved netbios name from cache") state['name'] = host.registry['netbios_name'] end nbcache_mutex "done" stdnse.debug2("SMB: Starting SMB2 session for %s (%s)", host.name, host.ip) if(port == nil) then return false, "SMB: Couldn't find a valid port to check" end -- Initialize the accounts for logging on smbauth.init_account(host) if(port ~= 139) then status, state['socket'] = start_raw(host, port) state['port'] = port if(status == false) then return false, state['socket'] end return true, state else status, state['socket'] = start_netbios(host, port) state['port'] = port if(status == false) then return false, state['socket'] end return true, state end return false, "SMB: Couldn't find a valid port to check" end --- 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. -- --@param smb The SMB object associated with the connection --@return (status, result) If status is false, result is an error message. Otherwise, result -- is undefined. function stop(smb) if(smb['TreeId'] ~= 0) then tree_disconnect(smb) end if(smb['MessageId'] ~= -1) then logoff(smb) end stdnse.debug2("SMB: Closing socket") if(smb['socket'] ~= nil) then local status, err = smb['socket']:close() if(status == false) then return false, "SMB: Failed to close socket: " .. err end end return true end --- Begins a raw SMB session, likely over port 445. Since nothing extra is required, this -- function simply makes a connection and returns the socket. -- --@param host The host object to check. --@param port The port to use (most likely 445). --@return (status, socket) if status is true, result is the newly created socket. -- Otherwise, socket is the error message. function start_raw(host, port) local status, err local socket = nmap.new_socket() socket:set_timeout(TIMEOUT) status, err = socket:connect(host, port, "tcp") if(status == false) then return false, "SMB: Failed to connect to host: " .. err end return true, socket end --- Begins a SMB session over NetBIOS. -- -- This requires a NetBIOS Session Start message to be sent first, which in -- turn requires the NetBIOS name. The name can be provided as a parameter, or -- it can be automatically determined. -- -- Automatically determining the name is interesting, to say the least. Here -- are the names it tries, and the order it tries them in: -- * The name the user provided, if present -- * The name pulled from NetBIOS (udp/137), if possible -- * The generic name "*SMBSERVER" -- * Each subset of the domain name (for example, scanme.insecure.org would -- attempt "scanme", "scanme.insecure", and "scanme.insecure.org") -- -- This whole sequence is a little hackish, but it's the standard way of doing -- it. -- --@param host The host object to check. --@param port The port to use (most likely 139). --@param name [optional] The NetBIOS name of the host. Will attempt to -- automatically determine if it isn't given. --@return (status, socket) if status is true, result is the port -- Otherwise, socket is the error message. function start_netbios(host, port, name) local i local status, err local pos, result, flags, length local socket = nmap.new_socket() -- First, populate the name array with all possible names, in order of significance local names = {} -- Use the name parameter if(name ~= nil) then names[#names + 1] = name end -- Get the name of the server from NetBIOS status, name = netbios.get_server_name(host.ip) if(status == true) then names[#names + 1] = name end -- "*SMBSERVER" is a special name that any server should respond to names[#names + 1] = "*SMBSERVER" -- If all else fails, use each substring of the DNS name (this is a HUGE hack, but is actually -- a recommended way of doing this!) if(host.name ~= nil and host.name ~= "") then local new_names = get_subnames(host.name) for i = 1, #new_names, 1 do names[#names + 1] = new_names[i] end end -- This loop will try all the NetBIOS names we've collected, hoping one of them will work. Yes, -- this is a hackish way, but it's actually the recommended way. i = 1 repeat -- Use the current name name = names[i] -- Some debug information stdnse.debug1("SMB: Trying to start NetBIOS session with name = '%s'", name) -- Request a NetBIOS session local session_request = bin.pack(">CCSzz", 0x81, -- session request 0x00, -- flags 0x44, -- length netbios.name_encode(name), -- server name netbios.name_encode("NMAP") -- client name ); stdnse.debug3("SMB: Connecting to %s", host.ip) socket:set_timeout(TIMEOUT) status, err = socket:connect(host, port, "tcp") if(status == false) then socket:close() return false, "SMB: Failed to connect: " .. err end -- Send the session request stdnse.debug3("SMB: Sending NetBIOS session request with name %s", name) status, err = socket:send(session_request) if(status == false) then socket:close() return false, "SMB: Failed to send: " .. err end socket:set_timeout(TIMEOUT) -- Receive the session response stdnse.debug3("SMB: Receiving NetBIOS session response") status, result = socket:receive_buf(match.numbytes(4), true); if(status == false) then socket:close() return false, "SMB: Failed to close socket: " .. result end pos, result, flags, length = bin.unpack(">CCS", result) if(result == nil or length == nil) then return false, "SMB: ERROR: Server returned less data than it was supposed to (one or more fields are missing); aborting [1]" end -- Check for a positive session response (0x82) if result == 0x82 then stdnse.debug3("SMB: Successfully established NetBIOS session with server name %s", name) return true, socket end -- If the session failed, close the socket and try the next name stdnse.debug1("SMB: Session request failed, trying next name") socket:close() -- Try the next name i = i + 1 until i > #names -- We reached the end of our names list stdnse.debug1("SMB: None of the NetBIOS names worked!") return false, "SMB: Couldn't find a NetBIOS name that works for the server. Sorry!" end --- Creates a string containing a SMB packet header - async. The header looks like this: -- -- -- -------------------------------------------------------------------------------------------------- -- | 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 | -- -------------------------------------------------------------------------------------------------- -- | 0xFE | 'S' | 'M' | 'B' | -- -------------------------------------------------------------------------------------------------- -- | StructureSize | CreditCharge | -- -------------------------------------------------------------------------------------------------- -- | (ChannelSequence/Reserved)/Status | -- -------------------------------------------------------------------------------------------------- -- | Command | CreditRequest/CreditResponse | -- -------------------------------------------------------------------------------------------------- -- | Flags | -- -------------------------------------------------------------------------------------------------- -- | NextCommand | -- -------------------------------------------------------------------------------------------------- -- | MessageId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | AsyncId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | MessageId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | SessionId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | Signature | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- -- -- All fields are, incidentally, encoded in little endian byte order. -- -- For the purposes here, the program doesn't care about most of the fields so they're given default -- values. The "command" field is the only one we ever have to set manually, in my experience. The TID -- and UID need to be set, but those are stored in the smb state and don't require user intervention. -- --@param smb The smb state table. --@param command The command to use. --@param overrides The overrides table. Keep in mind that overriding things like flags is generally a very bad idea, unless you know what you're doing. --@return A binary string containing the packed packet header. function smb_encode_header_async(smb, command, overrides) -- TODO end --- Creates a string containing a SMB packet header - sync. The header looks like this: -- -- -- -------------------------------------------------------------------------------------------------- -- | 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 | -- -------------------------------------------------------------------------------------------------- -- | 0xFE | 'S' | 'M' | 'B' | -- -------------------------------------------------------------------------------------------------- -- | StructureSize | CreditCharge | -- -------------------------------------------------------------------------------------------------- -- | (ChannelSequence/Reserved)/Status | -- -------------------------------------------------------------------------------------------------- -- | Command | CreditRequest/CreditResponse | -- -------------------------------------------------------------------------------------------------- -- | Flags | -- -------------------------------------------------------------------------------------------------- -- | NextCommand | -- -------------------------------------------------------------------------------------------------- -- | MessageId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | Reserved | -- -------------------------------------------------------------------------------------------------- -- | TreeId | -- -------------------------------------------------------------------------------------------------- -- | SessionId | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | Signature | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- | ... | -- -------------------------------------------------------------------------------------------------- -- -- -- All fields are, incidentally, encoded in little endian byte order. -- -- For the purposes here, the program doesn't care about most of the fields so they're given default -- values. The "command" field is the only one we ever have to set manually, in my experience. The TID -- and UID need to be set, but those are stored in the smb state and don't require user intervention. -- --@param smb The smb state table. --@param command The command to use. --@param overrides The overrides table. Keep in mind that overriding things like flags is generally a very bad idea, unless you know what you're doing. --@return A binary string containing the packed packet header. function smb_encode_header_sync(smb, command, overrides) -- Make sure we have an overrides array overrides = overrides or {} -- Used for the ProtocolId local sig = "\xFESMB" local structureSize = 64 local flags = 0 if smb['MessageId'] then smb['MessageId'] = smb['MessageId'] + 1 end -- -- Pretty much every flags is deprecated. We set these two because they're required to be on. -- local flags = bit.bor(0x10, 0x08) -- SMB_FLAGS_CANONICAL_PATHNAMES | SMB_FLAGS_CASELESS_PATHNAMES -- -- These flags are less deprecated. We negotiate 32-bit status codes and long names. We also don't include Unicode, which tells -- -- the server that we deal in ASCII. -- local flags2 = bit.bor(0x4000, 0x2000, 0x0040, 0x0001) -- SMB_FLAGS2_32BIT_STATUS | SMB_FLAGS2_EXECUTE_ONLY_READS | SMB_FLAGS2_IS_LONG_NAME | SMB_FLAGS2_KNOWS_LONG_NAMES -- -- Unless the user's disabled the security signature, add it -- if(nmap.registry.args.smbsign ~= "disable") then -- flags2 = bit.bor(flags2, 0x0004) -- SMB_FLAGS2_SECURITY_SIGNATURE -- end -- -- TreeID should never ever be 'nil', but it seems to happen once in awhile so print an error -- if(smb['tid'] == nil) then -- return false, string.format("SMB: ERROR: TreeID value was set to nil on host %s", smb['ip']) -- end local header = bin.pack("smb_get_header. --@param data The data. --@param overrides Overrides table. --@return (result, err) If result is false, err is the error message. Otherwise, err is -- undefined function smb_send(smb, header, data, overrides) overrides = overrides or {} local body = header .. data local attempts = 5 local status, err local out = bin.pack(">II", netbios_data) if(netbios_length == nil) then return false, "SMB: ERROR: Server returned less data than it was supposed to (one or more fields are missing); aborting [2]" end -- Make the length 24 bits netbios_length = bit.band(netbios_length, 0x00FFFFFF) -- The total length is the netbios_length, plus 4 (for the length itself) length = netbios_length + 4 local attempts = 5 local smb_data repeat attempts = attempts - 1 status, smb_data = smb['socket']:receive_buf(match.numbytes(netbios_length), true) until(status or (attempts == 0)) -- Make sure the connection is still alive if(status ~= true) then return false, "SMB: Failed to receive bytes after 5 attempts: " .. smb_data end local result = netbios_data .. smb_data if(#result ~= length) then stdnse.debug1("SMB: ERROR: Received wrong number of bytes, there will likely be issues (received %d, expected %d)", #result, length) return false, string.format("SMB: ERROR: Didn't receive the expected number of bytes; received %d, expected %d. This will almost certainly cause some errors.", #result, length) end -- The header is 64 bytes. pos, header = bin.unpack("SMB2 NEGOTIATE, which is typically the first SMB packet sent out. function negotiate_protocol(smb, overrides) local header, data -- Make sure we have overrides overrides = overrides or {} header = smb_encode_header_sync(smb, command_codes['SMB2_COM_NEGOTIATE'], overrides) local StructureSize = 36 local DialectCount -- Data is a list of strings, terminated by a blank one. if(overrides['Dialects'] == nil) then DialectCount = 2 else DialectCount = #overrides['Dialects'] end local SecurityMode = overrides["SecurityMode"] or 0x01 local Reserved = 0 local Capabilities = overrides["SecurityMode"] or 0 local GUID1 = overrides["GUID1"] or 1 local GUID2 = overrides["GUID2"] or 1 local ClientStartTime = overrides["ClientStartTime"] or 0x010 local Dialects1 = 0x0202 local Dialects2 = 0x0210 local data = bin.pack(" 0 ) then pos, smb['SecurityBlob'] = bin.unpack(" 11 ) then local pos, oid = bin.unpack(">A6", smb['SecurityBlob'], 5) sp_nego = ( oid == "\x2b\x06\x01\x05\x05\x02" ) -- check for SPNEGO OID 1.3.6.1.5.5.2 end while result ~= false do -- These are loop variables local security_blob = nil local security_blob_length = 0 repeat -- Get the new security blob, passing the old security blob as a parameter. If there was no previous security blob, then nil is passed, which creates a new one if ( not(security_blob) ) then status, security_blob, smb['mac_key'] = smbauth.get_security_blob(security_blob, smb['ip'], username, domain, password, password_hash, hash_type, (sp_nego and 0x00088215)) if ( sp_nego ) then local enc = asn1.ASN1Encoder:new() local mechtype = enc:encode( { type = 'A0', value = enc:encode( { type = '30', value = enc:encode( { type = '06', value = bin.pack("H", "2b06010401823702020a") } ) } ) } ) local oid = enc:encode( { type = '06', value = bin.pack("H", "2b0601050502") } ) security_blob = enc:encode(security_blob) security_blob = enc:encode( { type = 'A2', value = security_blob } ) security_blob = mechtype .. security_blob security_blob = enc:encode( { type = '30', value = security_blob } ) security_blob = enc:encode( { type = 'A0', value = security_blob } ) security_blob = oid .. security_blob security_blob = enc:encode( { type = '60', value = security_blob } ) end else if ( sp_nego ) then if ( smb['domain'] or smb['server'] and ( not(domain) or #domain == 0 ) ) then domain = smb['domain'] or smb['server'] end hash_type = "ntlm" end status, security_blob, smb['mac_key'] = smbauth.get_security_blob(security_blob, smb['ip'], username, domain, password, password_hash, hash_type, (sp_nego and 0x00088215)) if ( sp_nego ) then local enc = asn1.ASN1Encoder:new() security_blob = enc:encode(security_blob) security_blob = enc:encode( { type = 'A2', value = security_blob } ) security_blob = enc:encode( { type = '30', value = security_blob } ) security_blob = enc:encode( { type = 'A1', value = security_blob } ) end end -- There was an error processing the security blob if(status == false) then return false, string.format("SMB: ERROR: Security blob: %s", security_blob) end local data = bin.pack(" 9) then return false, "SMB: ERROR: Server has too many active connections; giving up." end local backoff = math.random() * 10 stdnse.debug1("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.debug1("SMB: Extended login to %s as %s\\%s failed (%s)", smb['ip'], 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 result = false end end -- Loop over the accounts if(log_errors == nil or log_errors == true) then stdnse.debug1("SMB: ERROR: All logins failed, sorry it didn't work out!") end return false, status_name end function tree_connect(smb, path, overrides) overrides = overrides or {} local buffer = "" for i = 1, #path do buffer = buffer .. bin.pack("