Nmap Development mailing list archives
NSELib RFC -- NetBIOS / SMB
From: Ron <ron () skullsecurity net>
Date: Mon, 08 Sep 2008 21:34:57 -0500
Hey guys, As promised, I took apart smb-probe.nse and turned into a couple nselib libraries. For anybody who hasn't done nselibs before, I'll just say for the record -- it's extremely easy! Anyways, I included smb-probe.nse for now, and it still works as it did before, but it's only a demonstration of what smb.lua and netbios.lua do. I'm going to re-write it as a handful of smaller and more targeted scripts. So anyways, any thoughts on how this looks, or how it's set up? I'll post a couple useful scripts later this week, hopefully. Also, what's the best format to submit nselibs in, raw files or a patch? Thanks, Ron
--- Creates and parses NetBIOS traffic. The primary use for this is to send -- NetBIOS name requests. -- --@author Ron Bowes <ron () skullsecurity net> --@copyright See nmaps COPYING for licence ----------------------------------------------------------------------- module(... or "netbios", package.seeall) require 'bit' require 'bin' --- Encode a NetBIOS name for transport. Most packets that use the NetBIOS name -- require this encoding to happen first. It takes a name containing any possible -- character, and converted it to all uppercase characters (so it can, for example, -- pass case-sensitive data in a case-insensitive way) -- -- There are two levels of encoding performed:\n -- L1: Pad the string to 16 characters withs spaces (or NULLs if it's the -- wildcard "*") and replace each byte with two bytes representing each -- of its nibbles, plus 0x41. \n -- L2: Prepend the length to the string, and to each substring in the scope -- (separated by periods). \n --@param name The name that will be encoded (eg. "TEST1"). --@param scope [optional] The scope to encode it with. I've never seen scopes used -- in the real world (eg, "insecure.org"). --@return The L2-encoded name and scope -- (eg. "\x20FEEFFDFEDBCACACACACACACACACAAA\x08insecure\x03org") function name_encode(name, scope) -- Truncate or pad the string to 16 bytes if(string.len(name) > 16) then name = string.sub(name, 1, 16) else local padding = " " if name == "*" then padding = "\0" end repeat name = name .. padding until string.len(name) == 16 end -- Do the L1 encoding local L1_encoded = "" for i=1, string.len(name), 1 do local b = string.byte(name, i) L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0xF0), 4) + 0x41) L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0x0F), 0) + 0x41) end -- Do the L2 encoding local L2_encoded = string.char(32) .. L1_encoded if scope ~= nil then -- Split the scope at its periods local piece for piece in string.gmatch(scope, "[^.]+") do L2_encoded = L2_encoded .. string.char(string.len(piece)) .. piece end end return L2_encoded end --- Does the exact opposite of name_encode. Converts an encoded name to -- the string representation. If the encoding is invalid, it will still attempt -- to decode the string as best as possible. --@param encoded_name The L2-encoded name --@returns the decoded name and the scope. The name will still be padded, and the -- scope will never be nil (empty string is returned if no scope is present) function name_decode(encoded_name) local name = "" local scope = "" local len = string.byte(encoded_name, 1) local i for i = 2, len + 1, 2 do local ch = 0 ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i) - 0x41, 4)) ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i + 1) - 0x41, 0)) name = name .. string.char(ch) end -- Decode the scope local pos = 34 while string.len(encoded_name) > pos do local len = string.byte(encoded_name, pos) scope = scope .. string.sub(encoded_name, pos + 1, pos + len) .. "." pos = pos + 1 + len end -- If there was a scope, remove the trailing period if(string.len(scope) > 0) then scope = string.sub(scope, 1, string.len(scope) - 1) end return name, scope end --- Sends out a UDP probe on port 137 to get a human-readable list of names the -- the system is using. --@param host The IP or hostname to check. --@param prefix [optional] The prefix to put on each line when it's returned. --@return (status, result) If status is true, the result is a human-readable -- list of names. Otherwise, result is an error message. function get_names(host, prefix) local status, names, name_count = do_nbstat(host) if(prefix == nil) then prefix = "" end if(status) then local result = "" for i = 1, name_count, 1 do result = result .. string.format("%s%s<%02x>\n", prefix, names[i][1], names[i][2]) end return true, result else return false, names end end --- Sends out a UDP probe on port 137 to get the server's name (that is, the -- entry in its NBSTAT table with a 0x20 suffix). --@param host The IP or hostname of the server. --@return (status, result) If status is true, the result is the NetBIOS name. -- otherwise, result is an error message. function get_server_name(host) local status, names, name_count = do_nbstat(host) if(status) then local i for i = 1, name_count, 1 do if names[i][2] == 0x20 then return true, names[i][1] end end else return false, names end return false, "Couldn't find NetBIOS server name" end --- This is the function that actually handles the UDP query to retrieve -- the NBSTAT information. -- -- The NetBIOS request's header looks like this: -- --------------------------------------------------\n -- | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |\n -- | NAME_TRN_ID |\n -- | R | OPCODE | NM_FLAGS | RCODE | (FLAGS)\n -- | QDCOUNT |\n -- | ANCOUNT |\n -- | NSCOUNT |\n -- | ARCOUNT |\n -- --------------------------------------------------\n -- -- In this case, the TRN_ID is a constant (0x1337, what else?), the flags -- are 0, and we have one question. All fields are network byte order. -- -- The body of the packet is a list of names to check for in the following -- format: -- (ntstring) encoded name -- (2 bytes) query type (0x0021 = NBSTAT) -- (2 bytes) query class (0x0001 = IN) -- -- The response header is the exact same, except it'll have some flags set -- (0x8000 for sure, since it's a response), and ANCOUNT will be 1. The format -- of the answer is:\n -- (ntstring) requested name\n -- (2 bytes) query type\n -- (2 bytes) query class\n -- (2 bytes) time to live\n -- (2 bytes) record length\n -- (1 byte) number of names\n -- [for each name]\n -- (16 bytes) padded name, with a 1-byte suffix\n -- (2 bytes) flags\n -- --@param host The IP or hostname of the system. --@return (status, result) If status is true, then the servers names are -- returned as an array of (name, suffix, flags). -- Otherwise, result is an error message. function do_nbstat(host) local socket = nmap.new_socket() local encoded_name = name_encode("*") -- Create the query header local query = bin.pack(">SSSSSS", 0x1337, -- Transaction id 0x0000, -- Flags 1, -- Questions 0, -- Answers 0, -- Authority 0 -- Extra ) query = query .. bin.pack(">zSS", encoded_name, -- Encoded name 0x0021, -- Query type (0x21 = NBSTAT) 0x0001 -- Class = IN ) socket:connect(host, 137, "udp") socket:send(query) socket:set_timeout(1000) local status, result = socket:receive_bytes(1); socket:close() if(status) then local pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, rr_name, rr_type, rr_class, rr_ttl local rrlength, name_count pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT = bin.unpack(">SSSSSS", result) -- Sanity check the result (has to have the same TRN_ID, 1 answer, and proper flags) if(TRN_ID ~= 0x1337) then return false, string.format("Invalid transaction ID returned: 0x%04x", TRN_ID) end if(ANCOUNT ~= 1) then return false, "Server returned an invalid number of answers" end if(bit.band(FLAGS, 0x8000) == 0) then return false, "Server's flags didn't indicate a response" end if(bit.band(FLAGS, 0x0007) ~= 0) then return false, string.format("Server returned a NetBIOS error: 0x%02x", bit.band(FLAGS, 0x0007)) end -- Start parsing the answer field pos, rr_name, rr_type, rr_class, rr_ttl = bin.unpack(">zSSI", result, pos) -- More sanity checks if(rr_name ~= encoded_name) then return false, "Server returned incorrect name" end if(rr_class ~= 0x0001) then return false, "Server returned incorrect class" end if(rr_type ~= 0x0021) then return false, "Server returned incorrect query type" end pos, rrlength, name_count = bin.unpack(">SC", result, pos) local names = {} for i = 1, name_count do local name, suffix, flags -- Instead of reading the 16-byte name and pulling off the suffix, -- we read the first 15 bytes and then the 1-byte suffix. pos, name, suffix, flags = bin.unpack(">A15CS", result, pos) name = string.gsub(name, "[ ]*$", "") names[i] = { name, suffix, flags } end return true, names, name_count else return false, "Name query failed: " .. result end end
--- A library for SMB (Server Message Block) (aka CIFS) traffic. This traffic is normally -- sent to/from ports 139 or 445 of Windows systems, although it's also implemented by -- others (the most notable one being Samba). -- -- The intention of this library is toe ventually handle all aspects of the SMB protocol, -- A programmer using this library must already have some knowledge of the SMB protocol, -- although a lot isn't necessary. You can pick up a lot by looking at the code that uses -- this. The basic login is this: -- -- C->S SMB_COM_NEGOTIATE_PROTOCOL -- S->C SMB_COM_NEGOTIATE_PROTOCOL -- C->S SMB_COM_SESSION_SETUP_ANDX -- S->C SMB_COM_SESSION_SETUP_ANDX -- C->S SMB_COM_TREE_CONNCT_ANDX -- S->C SMB_COM_TREE_CONNCT_ANDX -- -- To initially begin the connection, there are two options: -- 1) Attempt to start a raw session over 445, if it's open. \n -- 2) Attempt to start a NetBIOS session over 139. Although the -- protocol's the same, it requires a "session request" packet. -- That packet requires the computer's name, which is requested -- using a NBSTAT probe over UDP port 137. \n -- -- Once it's connected, a SMB_COM_NEGOTIATE_PROTOCOL packet is sent, -- requesting the protocol "NT LM 0.12", which is the most commonly -- supported one. Among other things, the server's response contains -- the host's security level, the system time, and the computer/domain -- name. -- -- If that's successful, SMB_COM_SESSION_SETUP_ANDX is sent. It is essentially the logon -- packet, where the username, domain, and password are sent to the server for verification. -- The response to SMB_COM_SESSION_SETUP_ANDX is fairly simple, containing a boolean for -- success, along with the operating system and the lan manager name. -- -- After a successful SMB_COM_SESSION_START_ANDX has been made, a -- SMB_COM_TREE_CONNECT_ANDX packet can be sent. This is what connects to a share. -- The server responds to this with a boolean answer, and little more information. -- Each share will either return STATUS_BAD_NETWORK_NAME if the share doesn't -- exist, STATUS_ACCESS_DENIED if it exists but we don't have access, or -- STATUS_SUCCESS if exists and we do have access. -- -- Thanks go to Christopher R. Hertel and Implementing CIFS, which -- taught me everything I know about Microsoft's protocols. -- --@author Ron Bowes <ron () skullsecurity net> --@copyright See nmaps COPYING for licence ----------------------------------------------------------------------- module(... or "smb", package.seeall) require 'bit' require 'bin' require 'netbios' --- Begins a raw SMB session, likely over port 445. Since nothing extra is required, this -- function simply makes a connection and returns the socket. -- it off to smb_start(). -- [TODO] error handling -- --@param host The host (or IP) 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, result is the error message. function start_raw(host, port) local socket = nmap.new_socket() socket:connect(host, port, "tcp") 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. -- --@param host The host (or IP) 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, port) if status is true, result is the port -- Otherwise, result is the error message. function start_netbios(host, port, name) local pos, status, result, flags, length local socket = nmap.new_socket() if(name == nil) then -- Get the name of the server status, name = netbios.get_server_name(host) if(status == false) then return false, result end end -- Request a NetBIOS session session_request = bin.pack(">CCSzz", 0x81, -- session request 0x00, -- flags 0x44, -- length netbios.name_encode(name), -- server name netbios.name_encode("NMAP") -- client name ); socket:connect(host, port, "tcp") socket:send(session_request) socket:set_timeout(1000) -- Receive the session response status, result = socket:receive_bytes(4); pos, result, flags, length = bin.unpack(">CCS", result) -- Check for a position session response (0x82) if result ~= 0x82 then return false, "Server refused to grant a NetBIOS session" end return true, socket end --- Creates a string containing a SMB packet header. The header looks like this:\n -- --------------------------------------------------------------------------------------------------\n -- | 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 |\n -- --------------------------------------------------------------------------------------------------\n -- | 0xFF | 'S' | 'M' | 'B' |\n -- --------------------------------------------------------------------------------------------------\n -- | Command | Status... |\n -- --------------------------------------------------------------------------------------------------\n -- | ...Status | Flags | Flags2 |\n -- --------------------------------------------------------------------------------------------------\n -- | PID_high | Signature..... |\n -- --------------------------------------------------------------------------------------------------\n -- | ....Signature.... |\n -- --------------------------------------------------------------------------------------------------\n -- | ....Signature | Unused |\n -- --------------------------------------------------------------------------------------------------\n -- | TID | PID |\n -- --------------------------------------------------------------------------------------------------\n -- | UID | MID |\n -- ------------------------------------------------------------------------------------------------- \n -- -- All fields are, incidentally, encoded in little endian byte order. \n --\n -- For the purposes here, the program doesn't care about most of the fields so they're given default \n -- values. The fields of interest are:\n -- * Command -- The command of the packet (SMB_COM_NEGOTIATE, SMB_COM_SESSION_SETUP_ANDX, etc)\n -- * UID/TID -- Sent by the server, and just have to be echoed back\n --@param command The command to use. --@param uid The UserID, which is returned by SMB_COM_SESSION_SETUP_ANDX (0 otherwise) --@param tid The TreeID, which is returned by SMB_COM_TREE_CONNECT_ANDX (0 otherwise) --@return A binary string containing the packed packet header. local function smb_encode_header(command, uid, tid) -- Used for the header local smb = string.char(0xFF) .. "SMB" -- 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, 0x0040, 0x0001) -- SMB_FLAGS2_32BIT_STATUS | SMB_FLAGS2_IS_LONG_NAME | SMB_FLAGS2_KNOWS_LONG_NAMES local header = bin.pack("<CCCCCICSSLSSSSS", smb:byte(1), -- Header smb:byte(2), -- Header smb:byte(3), -- Header smb:byte(4), -- Header command, -- Command 0, -- status flags, -- flags flags2, -- flags2 0, -- extra (pid_high) 0, -- extra (signature) 0, -- extra (unused) tid, -- tid 0, -- pid uid, -- uid 0 -- mid ) return header end --- Converts a string containing the parameters section into the encoded parameters string. -- The encoding is simple:\n -- (1 byte) The number of 2-byte values in the parameters section\n -- (variable) The parameter section\n -- This is automatically done by smb_send(). -- -- @param parameters The parameters section. -- @return The encoded parameters. local function smb_encode_parameters(parameters) return bin.pack("<CA", string.len(parameters) / 2, parameters) end --- Converts a string containing the data section into the encoded data string. -- The encoding is simple:\n -- (2 bytes) The number of bytes in the data section\n -- (variable) The data section\n -- This is automatically done by smb_send(). -- -- @param data The data section. -- @return The encoded data. local function smb_encode_data(data) return bin.pack("<SA", string.len(data), data) end --- Prepends the NetBIOS header to the packet, which is essentially the length, encoded -- in 4 bytes of big endian, and sends it out. The length field is actually 17 or 24 bits -- wide, depending on whether or not we're using raw, but that shouldn't matter. -- [TODO] Error checking -- --@param socket The socket to send the packet on. --@param header The header, encoded with smb_get_header(). --@param parameters The parameters --@param data The data function smb_send(socket, header, parameters, data) local encoded_parameters = smb_encode_parameters(parameters) local encoded_data = smb_encode_data(data) local len = string.len(header) + string.len(encoded_parameters) + string.len(encoded_data) local out = bin.pack(">I<AAA", len, header, encoded_parameters, encoded_data) socket:send(out) end --- Reads the next packet from the socket, and parses it into the header, parameters, -- and data. -- [TODO] This assumes that exactly one packet arrives, which may not be the case. -- Some buffering should happen here. Currently, we're waiting on 32 bytes, which -- is the length of the header, but there's no guarantee that we get the entire -- body. --@param socket The socket to read the packet from --@return (status, header, parameters, data) If status is true, the header, -- parameters, and data are all the raw arrays (with the lengths already -- removed). If status is false, header contains an error message and parameters/ -- data are undefined. function smb_read(socket) local status, result local pos, length, header, parameter_length, parameters, data_length, data -- Receive the response -- [TODO] set the timeout length per jah's strategy: -- http://seclists.org/nmap-dev/2008/q3/0702.html socket:set_timeout(1000) status, result = socket:receive_bytes(32); -- Make sure the connection is still alive if(status ~= true) then return false, result end -- The length of the packet is 4 bytes of big endian (for our purposes). -- The header is 32 bytes. pos, length, header = bin.unpack(">I<A32", result) -- The parameters length is a 1-byte value. pos, parameter_length = bin.unpack("<C", result, pos) -- Double the length parameter, since parameters are two-byte values. pos, parameters = bin.unpack(string.format("<A%d", parameter_length*2), result, pos) -- The data length is a 2-byte value. pos, data_length = bin.unpack("<S", result, pos) -- Read that many bytes of data. pos, data = bin.unpack(string.format("<A%d", data_length), result, pos) return status, header, parameters, data end --- Sends out SMB_COM_NEGOTIATE_PROTOCOL, which is typically the first SMB packet sent out. -- Sends the following:\n -- * List of known protocols\n --\n -- Receives:\n -- * The prefered dialect\n -- * The security mode\n -- * Max number of multiplexed connectiosn, virtual circuits, and buffer sizes\n -- * The server's system time and timezone\n -- * The "encryption key" (aka, the server challenge)\n -- * The capabilities\n -- * The server and domain names\n --@param socket The socket, in the proper state (ie, newly connected). --@return (status, result) If status is false, result is an error message. Otherwise, result is a -- table with the following elements:\n -- 'security_mode' Whether or not to use cleartext passwords, message signatures, etc.\n -- 'max_mpx' Maximum number of multiplexed connections\n -- 'max_vc' Maximum number of virtual circuits\n -- 'max_buffer' Maximum buffer size\n -- 'max_raw_buffer' Maximum buffer size for raw connections (considered obsolete)\n -- 'session_key' A value that's basically just echoed back\n -- 'capabilities' The server's capabilities\n -- 'time' The server's time (in UNIX-style seconds since 1970)\n -- 'date' The server's date in a user-readable format\n -- 'timezone' The server's timezone, in hours from UTC\n -- 'timezone_str' The server's timezone, as a string\n -- 'server_challenge' A random string used for challenge/response\n -- 'domain' The server's primary domain\n -- 'server' The server's name\n function smb_negotiate_protocol(socket) local header, parameters, data local pos local header1, header2, header3, ehader4, command, status, flags, flags2, pid_high, signature, unused, pid, mid local dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, session_key, capabilities, time, timezone, key_length local server_challenge, date, timezone_str local domain, server local response = {} header = smb_encode_header(0x72, 0, 0) -- Parameters are blank parameters = "" -- Data is a list of strings, terminated by a blank one. data = bin.pack("<CzCz", 2, "NT LM 0.12", 2, "") -- Send the negotiate request smb_send(socket, header, parameters, data) -- Read the result status, header, parameters, data = smb_read(socket) if(status ~= true) then return false, header end -- Since this is our first response, parse out the header pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header) -- Parse the parameter section pos, dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, session_key, capabilities, time, timezone, key_length = bin.unpack("<SCSSIIIILsC", parameters) -- Convert the time and timezone to more useful values time = (time / 10000000) - 11644473600 date = os.date("%Y-%m-%d %H:%M:%S", time) timezone = -(timezone / 60) if(timezone == 0) then timezone_str = "UTC+0" elseif(timezone < 0) then timezone_str = "UTC-" .. math.abs(timezone) else timezone_str = "UTC+" .. timezone end -- Data section -- This one's a little messier, because I don't appear to have unicode support pos, server_challenge = bin.unpack(string.format("<A%d", key_length), data) -- Get the domain as a Unicode string local ch, dummy domain = "" pos, ch, dummy = bin.unpack("<CC", data, pos) while ch ~= 0 do domain = domain .. string.char(ch) pos, ch, dummy = bin.unpack("<CC", data, pos) end -- Get the server name as a Unicode string server = "" pos, ch, dummy = bin.unpack("<CC", data, pos) while ch do server = server .. string.char(ch) pos, ch, dummy = bin.unpack("<CC", data, pos) end -- Fill out response variables response['security_mode'] = security_mode response['max_mpx'] = max_mpx response['max_vc'] = max_vc response['max_buffer'] = max_buffer response['max_raw_buffer'] = max_raw_buffer response['session_key'] = session_key response['capabilities'] = capabilities response['time'] = time response['date'] = date response['timezone'] = timezone response['timezone_str'] = timezone_str response['server_challenge'] = server_challenge response['domain'] = domain response['server'] = server return true, response end --- Sends out SMB_COM_SESSION_START_ANDX, which attempts to log a user in. -- Sends the following:\n -- * Negotiated parameters (multiplexed connections, virtual circuit, capabilities)\n -- * Passwords (plaintext, unicode, lanman, ntlm, lmv2, ntlmv2, etc)\n -- * Account name\n -- * OS (I just send "Nmap")\n -- * Native LAN Manager (no clue what that is, but it seems to be ignored)\n --\n -- Receives the following:\n -- * User ID\n -- * Server OS\n --\n --@param socket The socket, in the proper state (ie, after protocol has been negotiated). --@param username The account name to use. For Null sessions, leave it blank (''). --@param session_key The session_key value, returned by SMB_COM_NEGOTIATE_PROTOCOL. --@param capabilities The server's capabilities, returned by SMB_COM_NEGOTIATE_PROTOCOL. --@return (status, result) If status is false, result is an error message. Otherwise, result is a -- table with the following elements:\n -- 'uid' The UserID for the session -- 'is_guest' If set, the username wasn't found so the user was automatically logged in -- as the guest account -- 'os' The operating system -- 'lanmanager' The servers's LAN Manager function smb_start_session(socket, username, session_key, capabilities) local status, result local header, parameters, data local pos local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, uid, mid local andx_command, andx_reserved, andx_offset, action local os, lanmanager, domain local response = {} header = smb_encode_header(0x73, 0, 0) -- Parameters parameters = bin.pack("<CCSSSSISSII", 0xFF, -- ANDX -- no further commands 0x00, -- ANDX -- Reserved (0) 0x0000, -- ANDX -- next offset 0x1000, -- Max buffer size 0x0001, -- Max multiplexes 0x0000, -- Virtual circuit num session_key, -- The session key 0, -- ANSI/Lanman password length 0, -- Unicode/NTLM password length 0, -- Reserved capabilities -- Capabilities ) -- Data is a list of strings, terminated by a blank one. data = bin.pack("<zzzz", -- ANSI/Lanman password -- Unicode/NTLM password username, -- Account "", -- Domain "Nmap", -- OS "Native Lanman" -- Native LAN Manager ) -- Send the session setup request smb_send(socket, header, parameters, data) -- Read the result status, header, parameters, data = smb_read(socket) if(status ~= true) then return false, header end -- Check if we were allowed in pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header) if(status ~= 0) then return false, status end -- Parse the parameters pos, andx_command, andx_reserved, andx_offset, action = bin.unpack("<CCSS", parameters) -- Parse the data pos, os, lanmanager, domain = bin.unpack("<zzz", data) -- Fill in the response string response['uid'] = uid response['is_guest'] = bit.band(action, 1) response['os'] = os response['lanmanager'] = lanmanager return true, response end --- Sends out SMB_COM_SESSION_TREE_CONNECT_ANDX, which attempts to connect to a share. -- Sends the following:\n -- * Password (for share-level security, which we don't support)\n -- * Share name\n -- * Share type (or "?????" if it's unknown, that's what we do)\n --\n -- Receives the following:\n -- * Tree ID\n --\n --@param socket The socket, in the proper state. --@param path The path to connect (eg, \\servername\C$) --@param uid The UserID, returned by SMB_COM_SESSION_SETUP_ANDX --@return (status, result) If status is false, result is an error message. Otherwise, result is a -- table with the following elements:\n -- 'tid' The TreeID for the session function smb_tree_connect(socket, path, uid) local response = "" local header, parameters, data local pos local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid local andx_command, andx_reserved, andx_offset, action local response = {} header = smb_encode_header(0x75, uid, 0) parameters = bin.pack("<CCSSS", 0xFF, -- ANDX no further commands 0x00, -- ANDX reserved 0x0000, -- ANDX offset 0x0000, -- flags 0x0000 -- password length (for share-level security) ) data = bin.pack("zz", -- Share-level password path, -- Path "?????" -- Type of tree ("?????" = any) ) -- Send the tree connect request smb_send(socket, header, parameters, data) -- Read the result status, header, parameters, data = smb_read(socket) if(status ~= true) then return false, header end -- Check if we were allowed in pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header) if(status ~= 0) then return false, status end response['tid'] = tid return true, response end
--- Probes a system running SMB (CIFS) for more information. -- -- Currently serving only as a demonstration stripped, will be re-tasked -- shortly. -- --@usage -- nmap --script smb-probe.nse -p445 127.0.0.1\n -- sudo nmap -sU -sS --script smb-probe.nse -p U:137,T:139 127.0.0.1\n -- --@output -- Against a weak box: -- Host script results:\n -- | Probe SMB for information: \n -- | SMB Security: User-level authentication\n -- | SMB Security: Challenge/response passwords supported\n -- | SMB Security: Message signing supported\n -- | System time from SMB: 2008-09-07 00:56:02 [UTC-5]\n -- | Computer name from SMB: WORKGROUP\TEST1\n -- | OS detection from SMB: Windows 2000\n -- | Null sessions enabled\n -- | Found a share 'TEST'\n -- | Found a share 'TEST$'\n -- | Guest account enabled\n -- |_ Guest account has access to share 'TEST'\n --\n -- Against a more locked down host:\n -- Host script results:\n -- | Probe SMB for information: \n -- | SMB Security: User-level authentication\n -- | SMB Security: Challenge/response passwords supported\n -- | SMB Security: Message signing required\n -- | System time from SMB: 2008-09-07 01:55:46 [UTC-5]\n -- | Computer name from SMB: WORKGROUP\TEST2\n -- | OS detection from SMB: Windows 2000\n -- | Null sessions disabled\n -- |_ Guest account disabled\n -- ----------------------------------------------------------------------- id = "Probe SMB for information" description = "Elicits information from a host running NetBIOS/SMB" author = "Ron Bowes" copyright = "Ron Bowes" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"version","intrusive"} require 'bit' require 'bin' require 'netbios' require 'smb' -- The address being scanned local address -- Shares to try connecting to as Null session / GUEST local shares = {"C", "D", "TEST", "SHARE", "HOME"} --- Check whether or not this script should be fun. This script should -- run under two different conditions: -- a) port tcp/445 is open (allowing us to make a raw connection)\n -- b) ports tcp/139 and udp/137 are open (137 may not be known)\n hostrule = function(host) local port_u137 = nmap.get_port_state(host, {number=137, protocol="udp"}) local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"}) local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"}) if(port_t445 ~= nil and port_t445.state == "open") then -- tcp/445 is open, we're good return true end if(port_t139 ~= nil and port_t139.state == "open") then -- tcp/139 is open, check uf udp/137 is open or unknown if(port_u137 == nil or port_u137.state == "open" or port_u137.state == "open|filtered") then return true end end return false end --- Calls the functions to send out the packets, and puts together most of the results. -- This is basically the core function in this script, and where most of the future -- additions will happen. -- -- Currently, it does the following:\n -- * Sends out SMB_COM_NEGOTIATE_PROTOCOL\n -- * Starts a NULL session\n -- * Parses the OS\n -- * Tries to connect to shares\n -- * Tries to start a GUEST session\n -- * Tries to connect to shares\n -- * Tries to start an Administrator session (with a blank password)\n --@param socket The socket to use for this connection (it is assumed that the function -- can go ahead and start sending SMB traffic, so if the socket requires any kind -- of startup, it has to be done already. function smb_start(host, port) local status, result local response = "" local os = "" local lanmanager = "" if(port == 445) then result, socket = smb.start_raw(host, port) else result, socket = smb.start_netbios(host, port) end if(result == false) then return false, socket end -- Negotiate protocol status, result = smb.smb_negotiate_protocol(socket) if(status == false) then return false, result end response = response .. string.format("Computer name from SMB: %s\\%s\n", result['domain'], result['server']) response = response .. string.format("System time from SMB: %s [%s]\n", result['date'], result['timezone_str']) -- Check the security mode if(bit.band(result['security_mode'], 1) == 1) then response = response .. "SMB Security: User-level authentication\n" else response = response .. "SMB Security: Share-level authentication\n" end -- Challenge/response supported? if(bit.band(result['security_mode'], 2) == 0) then response = response .. "SMB Security: Plaintext only\n" else response = response .. "SMB Security: Challenge/response passwords supported\n" end -- Message signing supported/required? if(bit.band(result['security_mode'], 8) == 8) then response = response .. "SMB Security: Message signing required\n" elseif(bit.band(result['security_mode'], 4) == 4) then response = response .. "SMB Security: Message signing supported\n" else response = response .. "SMB Security: Message signing not supported\n" end -- Start a null session status, result2 = smb.smb_start_session(socket, "", result['session_key'], result['capabilities']) if(status == true) then response = response .. string.format("OS detection from SMB: %s\n", get_windows_version(result2['os'])) response = response .. string.format("LAN Manager from SMB: %s\n", result2['lanmanager']) -- See if it can connect to "IPC$" status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address), result2['uid']) if(status == true) then response = response .. "Null sessions enabled\n" else response = response .. "Null sessions disabled\n" end -- Loop through a couple common shares, see if Null session has access for i,share in ipairs(shares) do -- Try and connect status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share), result2['uid']) if(status == true) then response = response .. string.format("Null account has access to share '%s'\n", share) elseif(result3 == 0xc0000022) then -- STATUS_ACCESS_DENIED response = response .. string.format("Found a share '%s'\n", share) end -- Try with a '$' on the end status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share), result2['uid']) if(status == true) then response = response .. string.format("Null account has access to share '%s$'\n", share) elseif(result3 == 0xc0000022) then -- STATUS_ACCESS_DENIED response = response .. string.format("Found a share '%s$'\n", share) end end end -- Start a guest session status, result2 = smb.smb_start_session(socket, "GUEST", result['session_key'], result['capabilities']) if(status == true) then -- See if it can connect to "IPC$" status = smb.smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address), result2['uid']) if(status == true) then response = response .. "Guest account enabled\n" else response = response .. "Guest account disabled\n" end -- Loop through a couple common shares, see if GUEST has access for i,share in ipairs(shares) do -- Try and connect status = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share), result2['uid']) if(status == true) then response = response .. string.format("Guest account has access to share '%s'\n", share) end -- Try with a '$' on the end status = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share), result2['uid']) if(status == true) then response = response .. string.format("Guest account has access to share '%s$'\n", share) end end else response = response .. "Guest account disabled\n" end -- Check if 'Administrator' has a blank password status, result2 = smb.smb_start_session(socket, "ADMINISTRATOR", result['session_key'], result['capabilities']) if(status == true) then response = response .. "Administrator account has a blank password\n" elseif(result2 == 0xc000006e) then -- STATUS_ACCOUNT_RESTRICTION response = response .. "Administrator account has a blank password (but can't use SMB)" end return true, response end --- Converts numbered Windows versions (5.0, 5.1) to the names (Windows 2000, Windows XP). --@param os The name of the OS --@return The actual name of the OS (or the same as the 'os' parameter) function get_windows_version(os) if(os == "Windows 5.0") then return "Windows 2000" elseif(os == "Windows 5.1")then return "Windows XP" end return os end action = function(host) local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"}) local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"}) address = host.ip if(port_t445 ~= nil and port_t445.state == "open") then status, result = smb_start(host.ip, 445) else status, result = smb_start(host.ip, 139) end return string.format(" \n%s", result) end
_______________________________________________ Sent through the nmap-dev mailing list http://cgi.insecure.org/mailman/listinfo/nmap-dev Archived at http://SecLists.Org
Current thread:
- NSELib RFC -- NetBIOS / SMB Ron (Sep 08)
- Re: NSELib RFC -- NetBIOS / SMB Fyodor (Sep 08)
- Re: NSELib RFC -- NetBIOS / SMB Kris Katterjohn (Sep 09)
- Re: NSELib RFC -- NetBIOS / SMB Ron (Sep 09)