Nmap Development mailing list archives
[NSE RFC] SMB Probe
From: Ron <ron () skullsecurity net>
Date: Sun, 07 Sep 2008 13:27:33 -0500
Hey all, I spent the weekend writing a new SMB script. It's designed to replace "netbios-smb-os-discovery.nse" and can also replace "nbstat.nse" fairly easily, since by necessity it duplicates the functionality. I used nsedoc comments throughout, including a lengthy description field, so I won't repeat myself here. Check out the first bit for a bunch of information. Since I don't have a nsedoc parse, I might be doing it totally wrong, so let me know. I'm interested in comments on the style and such. I'm new to Lua, but I've been picking it up. There may be things about it I don't know, and I'm interested in learning. For example, I only just realized that 'local' is important, after debugging something nasty! The other bit I'm unsure about is the output. Right now, it builds the string as it goes along, but I might change it to build an array of strings instead. It's also a little chatty at the moment, although I think everything it displays is important. I might up the verbosity on some of it, though. Anyways, this works well against all my test boxes, and I kept it pretty clean (using pack/unpack to build packets, for example). I plan to expand this far more in the future, this is just the basic stuff. I'd appreciate output, though, and I hope to get a version done soon that can be included. Anyways, here are some examples...... -- Against a default Win2k box, over SMB -- $ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p445 192.168.1.41 Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:22 CDT Interesting ports on 192.168.1.41: PORT STATE SERVICE 445/tcp open microsoft-ds Host script results: | Probe SMB for information: (using port 445): | SMB Security: User-level authentication | SMB Security: Challenge/response passwords supported | SMB Security: Message signing supported | System time from SMB: 2008-09-07 00:59:47 [UTC-5] | Computer name from SMB: WORKGROUP\TEST1 | OS detection from SMB: Windows 2000 | Null sessions enabled | Found a share 'TEST' | Found a share 'TEST$' | Guest account enabled |_ Guest account has access to share 'TEST' Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds -- -- Against a locked down Win2k box, over NetBIOS -- $ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139 192.168.1.42 Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:13 CDT Interesting ports on 192.168.1.42: PORT STATE SERVICE 139/tcp open netbios-ssn Host script results: | Probe SMB for information: (using port 139): | SMB Security: User-level authentication | SMB Security: Challenge/response passwords supported | SMB Security: Message signing required | System time from SMB: 2008-09-07 13:13:41 [UTC-5] | Computer name from SMB: WORKGROUP\TEST2 | OS detection from SMB: Windows 2000 | Null sessions disabled |_ Guest account disabled Nmap done: 1 IP address (1 host up) scanned in 0.17 seconds -- -- Against Windows XP -- $ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139 192.168.1.44 Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:18 CDT Interesting ports on 192.168.1.44: PORT STATE SERVICE 139/tcp open netbios-ssn Host script results: | Probe SMB for information: (using port 139): | SMB Security: User-level authentication | SMB Security: Challenge/response passwords supported | SMB Security: Message signing not supported | System time from SMB: 2008-09-07 13:18:58 [UTC-5] | Computer name from SMB: WORKGROUP\DF | OS detection from SMB: Windows XP | Null sessions enabled |_ Guest account disabled Nmap done: 1 IP address (1 host up) scanned in 1.06 seconds -- -- Against Windows Server 2003 (with a blank admin password, apparently) -- $ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139 192.168.1.45 Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:20 CDT Interesting ports on 192.168.1.45: PORT STATE SERVICE 139/tcp open netbios-ssn Host script results: | Probe SMB for information: (using port 139): | SMB Security: User-level authentication | SMB Security: Challenge/response passwords supported | SMB Security: Message signing not supported | System time from SMB: 2008-08-23 10:17:17 [UTC-5] | Computer name from SMB: WORKGROUP\RON-PENTEST | OS detection from SMB: Windows Server 2003 3790 Service Pack 2 | Null sessions enabled | Guest account disabled |_ Administrator account has a blank password (but can't use SMB) Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds -- -- Against Windows Vista -- $ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p445 192.168.2.123 Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:23 CDT Interesting ports on 192.168.2.123: PORT STATE SERVICE 445/tcp open microsoft-ds Host script results: | Probe SMB for information: (using port 445): | SMB Security: User-level authentication | SMB Security: Challenge/response passwords supported | SMB Security: Message signing not supported | System time from SMB: 2008-09-07 10:57:20 [UTC-7] | Computer name from SMB: WORKGROUP\RON-PC | OS detection from SMB: Windows Vista (TM) Ultimate 6000 | Null sessions enabled |_ Guest account disabled Nmap done: 1 IP address (1 host up) scanned in 1.70 seconds --
--- Probes a system running SMB (CIFS) for more information. -- -- First, a connection to SMB has to be made. This script will try two -- ways (in this order):\n -- 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. The security level is whether or not the server supports -- challenge/response passwords, and whether or not it supports/requires -- message signatures (if they aren't required, a host is more vulnerable -- to man-in-the-middle attacks). -- -- If that's successful, it next sends a series of SMB_COM_SESSION_START_ANDX -- requests. It attempts to start sessions as the following users:\n -- 1) [blank] (Null session)\n -- 2) Guest\n -- 3) Administrator\n -- All with blank passwords. Typically, if the administrator account has -- a blank password, it can't be used over SMB. However, a different error -- code is returned if it's a blank password compared to the wrong password, -- so we make note. -- -- Another point of interest in the response to SMB_COM_SESSION_START_ANDX -- is the system's OS, so we make note of that, as well (thanks to Judy Novak -- and Sourcefire Inc for the idea, in the original SMB script). -- -- After a successful SMB_COM_SESSION_START_ANDX for a Null session or guest, -- a bunch of SMB_COM_TREE_CONNECT_ANDX requests are sent out. There is an -- array of shares to try and access called 'shares', and each of them is tried -- with and without a trailing dollar sign ('$'). The shares currently included -- are:\n -- * C\n -- * D\n -- * TEST\n -- * SHARE\n -- * HOME\n -- There is absolutely no reason I picked those, they seemed like they'd be -- common. The IPC$ share is also checked, but since everybody has access to -- it, it's only used to determine whether or not Null sessions are disabled. -- -- 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. -- --@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' -- The address being scanned local address -- The key for the session, which is simply echoed back local session_key -- The capabilities for the session, which are echoed back local capabilities -- The "encryption key", aka, the server challenge local server_challenge -- Set to the most recent user logged in (from SMB_COM_SESSION_SETUP_ANDX) local uid = 0 -- Set to the most recent tree connected to (from SMB_COM_TREE_CONNECT_ANDX) local tid = 0 -- 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 --- 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 netbios_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 netbios_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 netbios_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 netbios_get_names(host, prefix) local status, names, name_count = netbios_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 netbios_get_server_name(host) local status, names, name_count = netbios_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 netbios_do_nbstat(host) local socket = nmap.new_socket() local encoded_name = netbios_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 --- 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. -- -- For the purposes here, the program doesn't care about most of the fields so they're given default -- 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. --@return A string containing the packed packet header. function smb_get_header(command) -- 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 -- @param parameters The parameters section. -- @return The encoded parameters. function smb_get_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 -- @param data The data section. -- @return The encoded data. function smb_get_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 is actually 17 or 24 bytes, -- depending on whether or not we're using raw, but that shouldn't matter. --@param socket The socket to send the packet on. --@param header The header, encoded with smb_get_header(). --@param parameters The parameters, encoded with smb_get_parameters(). --@param data The data, encoded with, you guessed it, smb_get_data(). function smb_send(socket, header, parameters, data) local len = string.len(header) + string.len(parameters) + string.len(data) local out = bin.pack(">I<AAA", len, header, parameters, 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 and such 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 --- Begins doing the SMB checks over a raw SMB connection (probably port 445). Since we -- don't need a header or session start or anything, we just make a conenction and pass -- it off to smb_start(). --@param host The host (or IP) to check. --@param port The port to use (most likely 445). --@return (status, result) if status is true, result is the string to display to the user. -- Otherwise, result is the error message. function smb_start_raw(host, port) local socket = nmap.new_socket() socket:connect(host, port, "tcp") return smb_start(socket) end --- Begins doing the SMB checks over a NetBIOS connection (probably port 139). To call this -- function, the NetBIOS name has to be known. The netbios_get_server_name() function can -- generally pull that value, if it isn't known. --@param host The host (or IP) to check. --@param port The port to use (most likely 445). --@param name The NetBIOS name of the server. --@return (status, result) if status is true, result is the string to display to the user. -- Otherwise, result is the error message. function smb_start_netbios(host, port, name) local pos, status, result, flags, length local socket = nmap.new_socket() -- 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 smb_start(socket) 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(socket) local status, result local response = "" local os = "" -- Negotiate protocol status, result = smb_negotiate_protocol(socket) if(status == false) then return false, result end response = response .. result -- Start a null session status, os = smb_start_session(socket, "") if(status == true) then response = response .. string.format("OS detection from SMB: %s\n", get_windows_version(os)) -- See if it can connect to "IPC$" status = smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address)) 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, err = smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share)) if(status == true) then response = response .. string.format("Null account has access to share '%s'\n", share) elseif(err == 0xc0000022) then -- STATUS_ACCESS_DENIED response = response .. string.format("Found a share '%s'\n", share) end -- Try with a '$' on the end status = smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share)) if(status == true) then response = response .. string.format("Null account has access to share '%s$'\n", share) elseif(err == 0xc0000022) then -- STATUS_ACCESS_DENIED response = response .. string.format("Found a share '%s$'\n", share) end end end -- Start a guest session status, os = smb_start_session(socket, "GUEST") if(status == true) then -- See if it can connect to "IPC$" status = smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address)) 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_tree_connect(socket, string.format("\\\\%s\\%s", address, share)) 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_tree_connect(socket, string.format("\\\\%s\\%s$", address, share)) 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, err = smb_start_session(socket, "ADMINISTRATOR") if(status == true) then response = response .. "Administrator account has a blank password\n" elseif(err == 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 --- Sends out SMB_COM_NEGOTIATE_PROTOCOL, which requests a session. -- 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. --@return (status, result) If status is true, the result is a user-displayable string -- of the information cleaned. Otherwise, result is an error message. function smb_negotiate_protocol(socket) local response = "" 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, time, timezone, key_length local date, timezone_str local domain, server header = smb_get_header(0x72) -- Parameters are blank parameters = smb_get_parameters("") -- Data is a list of strings, terminated by a blank one. data = bin.pack("<CzCz", 2, "NT LM 0.12", 2, "") data = smb_get_data(data) -- 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) -- Check the security mode if(bit.band(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(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(security_mode, 8) == 8) then response = response .. "SMB Security: Message signing required\n" elseif(bit.band(security_mode, 4) == 4) then response = response .. "SMB Security: Message signing supported\n" else response = response .. "SMB Security: Message signing not supported\n" end -- 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 response = response .. string.format("System time from SMB: %s [%s]\n", date, timezone_str) -- 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 response = response .. string.format("Computer name from SMB: %s\\%s\n", domain, 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. --@return (status, result) If status is true, the result is a user-displayable string -- of the information cleaned. Otherwise, result is an error message. function smb_start_session(socket, username) local status, result 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 os, lanmanager, domain header = smb_get_header(0x73) -- 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 ) parameters = smb_get_parameters(parameters) -- 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 ) data = smb_get_data(data) -- 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) return true, os 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. --@return (status, result) If status is true, the result is a user-displayable string -- of the information cleaned. Otherwise, result is an error message. function smb_tree_connect(socket, path) 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 header = smb_get_header(0x75) parameters = bin.pack("<CCSSS", 0xFF, -- ANDX no further commands 0x00, -- ANDX reserved 0x0000, -- ANDX offset 0x0000, -- flags 0x0000 -- password length (for share-level security) ) parameters = smb_get_parameters(parameters) data = bin.pack("zz", -- Share-level password path, -- Path "?????" -- Type of tree ("?????" = any) ) data = smb_get_data(data) -- 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 return true 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 -- Start the probes, raw port = 445 status, result = smb_start_raw(host.ip, port) else -- Get the name status, name = netbios_get_server_name(host.ip) if(status == false) then return "Error: " .. result end -- Start the probes, with the NetBIOS header port = 139 status, result = smb_start_netbios(address, port, name) end return string.format("(using port %d):\n%s", port, result) end
_______________________________________________ Sent through the nmap-dev mailing list http://cgi.insecure.org/mailman/listinfo/nmap-dev Archived at http://SecLists.Org
Current thread:
- [NSE RFC] SMB Probe Ron (Sep 07)
- Re: [NSE RFC] SMB Probe Kris Katterjohn (Sep 07)
- Re: [NSE RFC] SMB Probe Ron (Sep 07)
- Re: [NSE RFC] SMB Probe Kris Katterjohn (Sep 07)
- Re: [NSE RFC] SMB Probe Ron (Sep 07)
- Message not available
- Message not available
- RE: [NSE RFC] SMB Probe Aaron Leininger (Sep 08)
- RE: [NSE RFC] SMB Probe Aaron Leininger (Sep 08)
- Message not available
- Re: [NSE RFC] SMB Probe Kris Katterjohn (Sep 07)