---
-- 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("