diff --git a/CHANGELOG b/CHANGELOG index d9bb9eb6d..b290b8353 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,17 @@ # Nmap Changelog ($Id$); -*-text-*- +o Added two new nselib modules, netbios and smb, that contain common + code for scripts using NetBIOS and SMB. Also added or updated four + scripts that use the new modules: + nbstat.nse: get NetBIOS names and MAC address. + smb-enum.nse: enumerate SMB users and shares. + smb-os-discovery.nse: get operating system over SMB (replaces + netbios-smb-os-discovery.nse). + smb-security-mode.nse: determine if a host uses user-level or + share-level security, and what other security features it + supports. + [Ron Bowes] + o A script could be executed twice if it was given with the --script option, also in the "version" category, and version detection (-sV) was requested. This has been fixed. [David] diff --git a/nselib/netbios.lua b/nselib/netbios.lua new file mode 100644 index 000000000..fb58e014c --- /dev/null +++ b/nselib/netbios.lua @@ -0,0 +1,396 @@ +--- Creates and parses NetBIOS traffic. The primary use for this is to send +-- NetBIOS name requests. +-- +--@author Ron Bowes +--@copyright See nmaps COPYING for licence +----------------------------------------------------------------------- + +module(... or "netbios", package.seeall) + +require 'bit' +require 'bin' +require 'stdnse' + +--- 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) + + stdnse.print_debug(3, "Encoding name '%s'", name) + -- 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 + + -- Convert to uppercase + name = string.upper(name) + + -- 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 + + stdnse.print_debug(3, "=> '%s'", L2_encoded) + 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 + + stdnse.print_debug(3, "Decoding name '%s'", encoded_name) + + 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 + + stdnse.print_debug(3, "=> '%s'", name) + + 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, statistics = do_nbstat(host) + + if(prefix == nil) then + prefix = "" + end + + + if(status) then + local result = "" + for i = 1, #names, 1 do + result = result .. string.format("%s%s<%02x>\n", prefix, names[i]['name'], names[i]['prefix']) + 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. +--@param names [optional] The names to use, from do_nbstat(). +--@return (status, result) If status is true, the result is the NetBIOS name. +-- otherwise, result is an error message. +function get_server_name(host, names) + + local status + local i + + if names == nil then + status, names = do_nbstat(host) + + if(status == false) then + return false, names + end + end + + for i = 1, #names, 1 do + if names[i]['suffix'] == 0x20 then + return true, names[i]['name'] + end + end + + return false, "Couldn't find NetBIOS server name" +end + +--- Sends out a UDP probe on port 137 to get the user's name (that is, the +-- entry in its NBSTAT table with a 0x03 suffix, that isn't the same as +-- the server's name. If the username can't be determined, which is frequently +-- the case, nil is returned. +--@param host The IP or hostname of the server. +--@param names [optional] The names to use, from do_nbstat(). +--@return (status, result) If status is true, the result is the NetBIOS name or nil. +-- otherwise, result is an error message. +function get_user_name(host, names) + + local status, server_name = get_server_name(host, names) + + if(status == false) then + return false, server_name + end + + if(names == nil) then + status, names = do_nbstat(host) + + if(status == false) then + return false, names + end + end + + for i = 1, #names, 1 do + if names[i]['suffix'] == 0x03 and names[i]['name'] ~= server_name then + return true, names[i]['name'] + end + end + + return true, nil + +end + + +--- This is the function that actually handles the UDP query to retrieve +-- the NBSTAT information. We make use of the Nmap registry here, so if another +-- script has already performed a nbstat query, the result can be re-used. +-- +-- 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 +-- (variable) statistics (usually mac addres) +-- +--@param host The IP or hostname of the system. +--@return (status, names, statistics) If status is true, then the servers names are +-- returned as a table containing 'name', 'suffix', and 'flags'. +-- Otherwise, names is an error message and statistics is undefined. +function do_nbstat(host) + + local status, err + local socket = nmap.new_socket() + local encoded_name = name_encode("*") + local statistics + + stdnse.print_debug(1, "Performing nbstat on host '%s'", host) + -- Check if it's cased in the registry for this host + if(nmap.registry["nbstat_names_" .. host] ~= nil) then + stdnse.print_debug(1, " [using cached value]") + return true, nmap.registry["nbstat_names_" .. host], nmap.registry["nbstat_statistics_" .. host] + end + + -- 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 + ) + status, err = socket:connect(host, 137, "udp") + if(status == false) then + return false, err + end + + status, err = socket:send(query) + if(status == false) then + return false, err + end + + socket:set_timeout(1000) + + status, result = socket:receive_bytes(1) + if(status == false) then + return false, result + end + + status, err = socket:close() + if(status == false) then + return false, err + end + + 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] = {} + names[i]['name'] = name + names[i]['suffix'] = suffix + names[i]['flags'] = flags + + -- Decrement the length + rrlength = rrlength - 18 + end + + pos, statistics = bin.unpack(string.format(">A%d", rrlength), result, pos) + + -- Put it in the registry, in case anybody else needs it + nmap.registry["nbstat_names_" .. host] = names + nmap.registry["nbstat_statistics_" .. host] = statistics + + return true, names, statistics + + else + return false, "Name query failed: " .. result + end +end + +---Convert the 16-bit flags field to a string. +--@param flags The 16-bit flags field +--@return A string representing the flags +function flags_to_string(flags) + local result = "" + + if(bit.band(flags, 0x8000) ~= 0) then + result = result .. "" + else + result = result .. "" + end + + if(bit.band(flags, 0x1000) ~= 0) then + result = result .. "" + end + + if(bit.band(flags, 0x0800) ~= 0) then + result = result .. "" + end + + if(bit.band(flags, 0x0400) ~= 0) then + result = result .. "" + end + + if(bit.band(flags, 0x0200) ~= 0) then + result = result .. "" + end + + return result +end + diff --git a/nselib/smb.lua b/nselib/smb.lua new file mode 100644 index 000000000..5a38f4b84 --- /dev/null +++ b/nselib/smb.lua @@ -0,0 +1,730 @@ +--- 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: +-- +-- [connect] +-- 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 +-- +-- In terms of functions here, the protocol is: +-- status, socket = smb.start(host) +-- status, negotiate_result = smb.negotiate_protocol(socket) +-- status, session_result = smb.start_session(socket, username, negotiate_result['session_key'], negotiate_result['capabilities']) +-- status, tree_result = smb.tree_connect(socket, path, session_result['uid']) +-- +-- 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 +--@copyright See nmaps COPYING for licence +----------------------------------------------------------------------- +module(... or "smb", package.seeall) + +require 'bit' +require 'bin' +require 'netbios' +require 'stdnse' + +mutex_id = "SMB" + +--- Determines whether or not SMB checks are possible on this host, and, if they are, +-- which port is best to use. This is how it decides:\n +--\n +-- a) If port tcp/445 is open, use it for a raw connection\n +-- b) Otherwise, if ports tcp/139 and udp/137 are open, do a NetBIOS connection. Since +-- UDP scanning isn't default, we're also ok with udp/137 in an unknown state. +-- +--@param host The host object. +--@return The port number to use, or nil if we don't have an SMB port +function get_port(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 445 + 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 139 + end + end + + return nil +end + +--- Begins a SMB session, automatically determining the best way to connect. Also starts a mutex +-- with mutex_id. This prevents multiple threads from making queries at the same time (which breaks +-- SMB). +-- +-- @param host The host object +-- @return (status, socket) if the status is true, result is the newly crated socket. +-- otherwise, socket is the error message. +function start(host) + local port = get_port(host) + local mutex = nmap.mutex(mutex_id) + + if(port == nil) then + return false, "Couldn't find a valid port to check" + end + + mutex "lock" + + if(port == 445) then + return start_raw(host, port) + elseif(port == 139) then + return start_netbios(host, port) + end + + return false, "Couldn't find a valid port to check" +end + +--- Kills the SMB connection, closes the socket, and releases the mutex. Because of the mutex +-- being released, a script HAS to call stop() before it exits, no matter why it's exiting! +-- +--@param socket The socket associated with the connection. +--@return (status, result) If status is false, result is an error message. Otherwise, result +-- is undefined. +function stop(socket) + local mutex = nmap.mutex(mutex_id) + + -- It's possible that the mutex wouldn't be created if there was an error condition. Therefore, + -- I'm calling 'trylock' first to ensure we have a lock on it. I'm not sure if that's the best + -- way to do this, though... + mutex "trylock" + mutex "done" + + stdnse.print_debug(2, "Closing SMB socket") + if(socket ~= nil) then + local status, err = socket:close() + + if(status == false) then + return false, 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. +-- it off to smb_start(). +-- +--@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() + + status, err = socket:connect(host.ip, port, "tcp") + + if(status == false) then + return false, err + end + + return true, socket +end + +--- This function will take a string like "a.b.c.d" and return "a", "a.b", "a.b.c", and "a.b.c.d". +-- This is used for discovering NetBIOS names. +--@param name The name to take apart +--@param list [optional] If list is set, names will be added to it then returned +--@return An array of the sub names +local function get_subnames(name) + local i = -1 + local list = {} + + repeat + local subname = name + + i = string.find(name, "[.]", i + 1) + if(i ~= nil) then + subname = string.sub(name, 1, i - 1) + end + + list[#list + 1] = string.upper(subname) + + until i == nil + + return list +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. \n +--\n +-- Automatically determining the name is interesting, to say the least. Here are the names +-- it tries, and the order it tries them in:\n +-- 1) The name the user provided, if present\n +-- 2) The name pulled from NetBIOS (udp/137), if possible\n +-- 3) The generic name "*SMBSERVER"\n +-- 4) Each subset of the domain name (for example, scanme.insecure.org would attempt "scanme", +-- "scanme.insecure", and "scanme.insecure.org")\n +--\n +-- 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 + 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.print_debug(1, "Trying to start NetBIOS session with name = '%s'", name) + -- 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 + ); + + stdnse.print_debug(3, "Connecting to %s", host.ip) + status, err = socket:connect(host.ip, port, "tcp") + if(status == false) then + socket:close() + return false, err + end + + -- Send the session request + stdnse.print_debug(3, "Sending NetBIOS session request with name %s", name) + status, err = socket:send(session_request) + if(status == false) then + socket:close() + return false, err + end + socket:set_timeout(1000) + + -- Receive the session response + stdnse.print_debug(3, "Receiving NetBIOS session response") + status, result = socket:receive_bytes(4); + if(status == false) then + socket:close() + return false, result + end + pos, result, flags, length = bin.unpack(">CCS", result) + + -- Check for a position session response (0x82) + if result == 0x82 then + stdnse.print_debug(3, "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.print_debug(3, "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.print_debug(3, "None of the NetBIOS names worked!") + return false, "Couldn't find a NetBIOS name that works for the server. Sorry!" +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("II Flags: +-- | Name: TEST1<20> Flags: +-- | Name: WORKGROUP<00> Flags: +-- | Name: TEST1<03> Flags: +-- | Name: WORKGROUP<1e> Flags: +-- | Name: RON<03> Flags: +-- | Name: WORKGROUP<1d> Flags: +-- |_ Name: \x01\x02__MSBROWSE__\x02<01> Flags: + id = "NBSTAT" description = "Sends a NetBIOS query to target host to try to determine \ -the NetBIOS name and MAC address." -author = "Brandon Enright " +the NetBIOS name and MAC address. For more information on the NetBIOS protocol, \ +see 'nselib/netbios.lua'." +author = "Brandon Enright , Ron Bowes" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" --- This script was created by reverse-engineering the packets --- sent by NBTSCAN and hacking with the Wireshark NetBIOS --- protocol dissector. I do not believe this constitutes --- a derivative work in the GPL sense of the phrase. - +-- Current version of this script was based entirly on Implementing CIFS, by +-- Christopher R. Hertel. categories = {"default", "discovery", "safe"} -require "comm" +require "netbios" -- I have excluded the port function param because it doesn't make much sense -- for a hostrule. It works without warning. The NSE documentation is @@ -48,139 +66,69 @@ hostrule = function(host) end --- Again, I have excluded the port param. Is this okay on a hostrule? action = function(host) - - -- This is the UDP NetBIOS request packet. I didn't feel like - -- actually generating a new one each time so this has been shamelessly - -- copied from a packet dump of nbtscan. - -- See http://www.unixwiz.net/tools/nbtscan.html for code. - -- The magic number in this code is \003\097. - local data = - "\003\097\000\016\000\001\000\000" .. - "\000\000\000\000\032\067\075\065" .. - "\065\065\065\065\065\065\065\065" .. - "\065\065\065\065\065\065\065\065" .. - "\065\065\065\065\065\065\065\065" .. - "\065\065\065\065\065\000\000\033" .. - "\000\001" - local status, result = comm.exchange(host, 137, data, {proto="udp", timeout=5000}) + local i + local status + local names, statistics + local server_name, user_name + local mac + local result = "" - if (not status) then - return + -- Get the list of NetBIOS names + status, names, statistics = netbios.do_nbstat(host.ip) + status, names, statistics = netbios.do_nbstat(host.ip) + status, names, statistics = netbios.do_nbstat(host.ip) + status, names, statistics = netbios.do_nbstat(host.ip) + if(status == false) then + return "ERROR: " .. names end - -- We got data back from 137, make sure we know it is open - nmap.set_port_state(host, {number=137, protocol="udp"}, "open") - - -- Magic numbers: - -- Offset to number of names returned: 57 - -- Useful name length: 15 - -- Name type length: 3 - -- Computer name type: \032\068\000 or \032\004\000 - -- User name type: \003\068\000 or \003\004\000 - -- Length of each name + name type: 19 - -- Length of MAC address: 6 - -- Note that string.sub includes a 0 char so these numbers are 1 less - - if (string.len(result) < 57) then - return + -- Get the server name + status, server_name = netbios.get_server_name(host.ip, names) + if(status == false) then + return "ERROR: " .. server_name end - -- Make sure the response at least looks like a NBTSTAT response - -- The first 2 bytes are the magic number sent originally, The second - -- 2 bytes should be 0x84 0x00 (errorless name query response) - if (string.sub(result, 1, 4) ~= "\003\097\132\000" ) then - return + -- Get the logged in user + status, user_name = netbios.get_user_name(host.ip, names) + if(status == false) then + return "ERROR: " .. user_name end - local namenum = string.byte(result, 57) - - if (string.len(result) < 58 + namenum * 18 + 6) then - return + -- Format the Mac address in the standard way + mac = string.format("%02x:%02x:%02x:%02x:%02x:%02x", statistics:byte(1), statistics:byte(2), statistics:byte(3), statistics:byte(4), statistics:byte(5), statistics:byte(6)) + -- Samba doesn't set the Mac address + if(mac == "00:00:00:00:00:00") then + mac = "" end + -- Check if we actually got a username + if(user_name == nil) then + user_name = "" + end - -- This loop will try to find the computer name. This name needs to - -- be found before the username because sometimes NetBIOS reports - -- username flags with the computer name as text. - local compname - for i = 0, namenum - 1, 1 do - -- Names come back trailing-space-padded so strip that off.. - local namefield = string.sub (result, 58 + i * 18, - 58 + i * 18 + 14) - local iname - local nameflags = string.sub (result, 58 + i * 18 + 15, - 58 + i * 18 + 15 + 2) - local padindex = string.find(namefield, " ") - if (padindex ~= nil and padindex > 1) then - iname = string.sub(namefield, 1, padindex - 1) - else - iname = namefield + result = result .. string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s\n", server_name, user_name, mac) + + -- If verbosity is set, dump the whole list of names + if(nmap.verbosity() >= 1) then + for i = 1, #names, 1 do + local padding = string.rep(" ", 17 - string.len(names[i]['name'])) + local flags_str = netbios.flags_to_string(names[i]['flags']) + result = result .. string.format("Name: %s<%02x>%sFlags: %s\n", names[i]['name'], names[i]['suffix'], padding, flags_str) end - if (nameflags == "\032\068\000" or - nameflags == "\032\004\000") then - compname = iname - end - end - - if (compname == nil) then - return - end - - - -- This loop will attempt to find the username logged onto the machine - -- This is not possible on most Windows machines (I don't know why) - -- Sometimes the flag that generally indicates the username - -- returns the computer name instead. This function will ignore - -- the username if it matches the computer name. This loop will not - -- properly report the the username if it really happens to be - -- the same as the computer name. - local username - for i = 0, namenum - 1, 1 do - -- Names come back trailing-space-padded so strip that off.. - local namefield = string.sub (result, 58 + i * 18, - 58 + i * 18 + 14) - local iname - local nameflags = string.sub (result, 58 + i * 18 + 15, - 58 + i * 18 + 15 + 2) - local padindex = string.find(namefield, " ") - if (padindex ~= nil and padindex > 1) then - iname = string.sub(namefield, 1, padindex - 1) - else - iname = namefield - end - - if (nameflags == "\003\068\000" or - nameflags == "\003\004\000") then - if (string.find(iname, compname, 1, true) == nil) then - username = iname + -- If super verbosity is set, print out the full statistics + if(nmap.verbosity() >= 2) then + result = result .. "Statistics: " + for i = 1, #statistics, 1 do + result = result .. string.format("%02x ", statistics:byte(i)) end + result = result .. "\n" end end - -- SAMBA likes to say its MAC is all 0s. That could be detected... - -- If people say printing a MAC of 0000.0000.000 is more wrong - -- than not returning a MAC at all then fix it here. - local macfield = string.sub (result, 58 + namenum * 18, - 58 + namenum * 18 + 5) - local mac = string.format ("%02X:%02X:%02X:%02X:%02X:%02X", - string.byte(macfield, 1), - string.byte(macfield, 2), - string.byte(macfield, 3), - string.byte(macfield, 4), - string.byte(macfield, 5), - string.byte(macfield, 6)) + return result - if (username ~= nil) then - return "NetBIOS name: " .. compname .. - ", NetBIOS user: " .. username .. - ", NetBIOS MAC: " .. mac - else - return "NetBIOS name: " .. compname .. - ", NetBIOS MAC: " .. mac - end end diff --git a/scripts/netbios-smb-os-discovery.nse b/scripts/netbios-smb-os-discovery.nse deleted file mode 100644 index f6d2a4bca..000000000 --- a/scripts/netbios-smb-os-discovery.nse +++ /dev/null @@ -1,456 +0,0 @@ ---- This script probes a target for its operating system version. --- It sends traffic via UDP port 137 and TCP port 139/445.\n\n --- == Implementation Information ==\n --- First, we need to --- elicit the NetBIOS share name associated with a workstation share. --- Once we have that, we need to encode the name into the "mangled" --- equivalent and send TCP 139/445 traffic to connect to the host and --- in an attempt to elicit the OS version name from an SMB Setup AndX --- response.\n\n --- --- Thanks to Michail Prokopyev and xSharez Scanner for required --- traffic to generate for OS version detection. --- ---@usage --- sudo nmap -sU -sS --script netbios-smb-os-discovery.nse -p U:137,T:139 127.0.0.1 ------------------------------------------------------------------------ - -id = "Discover OS Version over NetBIOS and SMB" -description = "Attempt to elicit OS version from host running NetBIOS/SMB" -author = "Judy Novak" -copyright = "Sourcefire Inc, (C) 2006-2007" -license = "Same as Nmap--See http://nmap.org/book/man-legal.html" -categories = {"version"} - -require 'bit' - -hostrule = function(host) - - -- This script should run under two different conditions: - -- a) port tcp/445 is open (allowing us to make a raw connection) - -- b) ports tcp/139 and udp/137 are open (137 may not be known) - - 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 - -action = function(host) - local sharename, message, osversion, currenttime, gen_msg, gen_msg_time, x - - sharename = 0 - osversion = "" - gen_msg = "OS version cannot be determined.\n" - gen_msg_time = "System time cannot be determined.\n" - - -- Decide whether to use raw SMB (port 445) or SMB over NetBIOS (139). - -- Raw is better, because it uses one less packet and doesn't require a - -- name to be known. - local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"}) - - local use_raw = (port_t445 ~= nil and port_t445.state == "open") - - if(not use_raw) then - sharename, message = udp_query(host) - end - - local ret = "" - - if (use_raw or sharename ~= 0) then - osversion, currenttime, message = tcp_session(sharename, host, use_raw) - if (osversion ~= 0) then - ret = ret .. osversion - if(currenttime ~= 0) then - ret = ret .. "\n" .. "Discover system time over SMB: " .. currenttime - else - ret = ret .. "\n" .. gen_msg_time .. message - end - else - ret = ret .. gen_msg .. message - end - else - ret = ret .. gen_msg .. "TCP/445 closed and couldn't determine NetBIOS name" - end - - return ret - -end - ------------------------------------------------------------------------ --- A NetBIOS wildcard query is sent to a host in an attempt to discover --- any NetBIOS shares on the host. - -function udp_query(host) - - local l, sharename, message - local WildCard = - string.char(0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x00) - - local socket = nmap.new_socket() - - socket:connect(host.ip, 137, "udp") - socket:send(WildCard) - socket:set_timeout(100) - - local status, result = socket:receive_bytes(1); - - socket:close() - - if (result ~= nil) then - l = string.len(result) - sharename = extract_sharename(result) - if (sharename ~= 0) then - return sharename, 1 - else - message = "Failed to find NetBIOS share name in response to UDP NetBIOS wildcard query" - return 0, message - end - end -end - ------------------------------------------------------------------------ --- This function extracts the name of a "workstation" share from the --- response to the UDP NetBIOS wildcard query. Typically, there are --- several share types returned, but only one with a "workstation" --- type/code can be queried later for the OS version. The workstation --- type/code is 0x44 0x00 for OS versions prior to Vista. The type/code --- for Vista is 0x04 0x00. - -function extract_sharename(resp) - - local lenpay, beg, eend, typebeg, typeend, temp, name, nametype, ntgeneric, ntvista, ename, myname, eename, ntunix - - beg = 58 - eend = beg + 15 - typebeg = eend + 1 - lenpay = string.len(resp) - - while (eend <= lenpay) do - - myname = string_concatenate(resp, beg, eend - 1) - nametype = string.byte(resp, typebeg) .. string.byte(resp, typebeg + 1) - ntgeneric = string.find(nametype, 0x44,0x00) - ntvista = string.find(nametype, 0x04, 0x00) - ntunix = string.find(nametype, 0x64, 0x00) - - if (ntgeneric == 1) or (ntvista == 1) or (ntunix == 1) then - ename = encode(myname) - end - - if (ename ~= nil) then - do - ename = string.char(0x20) .. ename .. string.char(0x43, 0x41, 0x00) - return(ename) - end - end - - beg = beg + 18 - eend = beg + 15 - typebeg = eend + 1 - end - return(0) -end - ------------------------------------------------------------------------ --- Extract multiple bytes from a string and return concatenated result - -function string_concatenate(mystring, start, stop) - local x, temp, newname - - for x = start, stop, 1 do - temp = string.byte(mystring,x) - if (x > start) then - newname = newname .. string.char(temp) - else - newname = string.char(temp) - end - end - return(newname) -end - ------------------------------------------------------------------------ --- This function encodes the workstation share name returned from the --- UDP wildcard NetBIOS query. Each character from the NetBIOS share --- name is encoded/mangled using a special algorithm. Rather than --- implementing the algorithm, Microsoft offers a conversion table for --- any valid character found in a share name. I could not figure out --- how to use a Lua dictionary where the key value included a --- non-alphanumeric character. The static variable chars represents --- most of the characters that can be found in a share and the position --- in the string "chars" is the corresponding position in the trtable --- table. The character " had to be handled separately as it is used --- to delimit the value of chars. - -encode = function(name) - - local ln, y, nchar, newname, pos, temp, trtable - local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !#$%&'()*+,-.=:;@^_{}~" - - local trtable = - { - string.char(0x45,0x42), string.char(0x45,0x43), string.char(0x45,0x44), string.char(0x45,0x45), string.char(0x45,0x46), - string.char(0x45,0x47), string.char(0x45,0x48), string.char(0x45,0x49), string.char(0x45,0x4A), string.char(0x45,0x4B), - string.char(0x45,0x4C), string.char(0x45,0x4D), string.char(0x45,0x4E), string.char(0x45,0x4F), string.char(0x45,0x50), - string.char(0x46,0x41), string.char(0x46,0x42), string.char(0x46,0x43), string.char(0x46,0x44), string.char(0x46,0x45), - string.char(0x46,0x46), string.char(0x46,0x47), string.char(0x46,0x48), string.char(0x46,0x49), string.char(0x46,0x4A), - string.char(0x46,0x4B), string.char(0x44,0x41), string.char(0x44,0x42), string.char(0x44,0x43), string.char(0x44,0x44), - string.char(0x44,0x45), string.char(0x44,0x46), string.char(0x44,0x47), string.char(0x44,0x48), string.char(0x44,0x49), - string.char(0x44,0x4A), string.char(0x43,0x41), string.char(0x43,0x42), string.char(0x43,0x44), string.char(0x43,0x45), - string.char(0x43,0x46), string.char(0x43,0x47), string.char(0x43,0x48), string.char(0x43,0x49), string.char(0x43,0x4A), - string.char(0x43,0x4B), string.char(0x43,0x4C), string.char(0x43,0x4D), string.char(0x43,0x4E), string.char(0x43,0x4F), - string.char(0x44,0x4E), string.char(0x44,0x4B), string.char(0x44,0x4C), string.char(0x45,0x41), string.char(0x46,0x4F), - string.char(0x46,0x50), string.char(0x48,0x4C), string.char(0x48,0x4E), string.char(0x48,0x4F) - } - - ln = string.len(name) - y = 1 - - while (y <= ln) do - temp = string.byte(name, y) - - if (temp == 0x00) then --Sharename must be followed by spaces not null's to be acceptable - return(nil) - elseif (temp == '"') then - nchar = string.char(0x43,0x43) - else do - temp = string.char(temp) - pos = string.find(chars, temp) - nchar = trtable[pos] - if (y > 1) then - newname = newname .. nchar - else - newname = nchar - end - y = y + 1 - end - end - end - return(newname) -end - ------------------------------------------------------------------------ --- This function invokes the TCP traffic that is generated to get --- a response that yields the OS version information. The first --- payload is an SMB session initiation request followed by a --- negotiate payload, and followed by a Session Setup AndX request. --- The workstation share name extracted from the UDP wildcard NetBIOS --- response must be used in the SMB session initiation request(payload 1). --- Payload for the requests that follow is static. - -function tcp_session(ename, host, use_raw) - - local catch = function() - socket:close() - end - - local rec1_payload, rec2_payload, rec3_payload, status, line1, line2, line3, currenttime, osversion, winshare, pos, message - - message = 0 - local win5 = "Windows 5.0" - local win51 = "Windows 5.1" - - winshare = string.char(0x20, 0x46, 0x48, 0x45, 0x4A, 0x45, 0x4F, 0x45, 0x45, 0x45, 0x50, 0x46, 0x48, 0x46, 0x44, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x00) - - rec1_payload = string.char(0x81, 0x00, 0x00, 0x44) .. ename .. winshare - - rec2_payload = string.char( 0x00, 0x00, 0x00, 0x85, 0xff, 0x53, 0x4d, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0 ) .. - string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfa ) .. - string.char( 0x00, 0x00, 0x17, 0x62, 0x00, 0x61, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f ) .. - string.char( 0x52, 0x4b, 0x20, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02 ) .. - string.char( 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x31, 0x2e, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6e, 0x64, 0x6f ) .. - string.char( 0x77, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70 ) .. - string.char( 0x73, 0x20, 0x33, 0x2e, 0x31, 0x61, 0x00, 0x02, 0x4c, 0x4d, 0x31, 0x2e, 0x32, 0x58, 0x30, 0x30 ) .. - string.char( 0x32, 0x00, 0x02, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x32, 0x2e, 0x31, 0x00, 0x02, 0x4e, 0x54 ) .. - string.char( 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 0x32, 0x00) - - rec3_payload = string.char( 0x00, 0x00, 0x00, 0xab, 0xff, 0x53, 0x4d, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0 ) .. - string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfa ) .. - string.char( 0x00, 0x00, 0x17, 0x62, 0x0d, 0xff, 0x00, 0x00, 0x00, 0x04, 0x11, 0x0a, 0x00, 0x00, 0x00, 0x00 ) .. - string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x6d ) .. - string.char( 0x00, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00 ) .. - string.char( 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00 ) .. - string.char( 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x39, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x39, 0x00 ) .. - string.char( 0x38, 0x00, 0x2f, 0x00, 0x4d, 0x00, 0x65, 0x00, 0x2f, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x2f, 0x00 ) .. - string.char( 0x32, 0x00, 0x6b, 0x00, 0x2f, 0x00, 0x58, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00 ) .. - string.char( 0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00, 0x65, 0x00, 0x7a, 0x00, 0x20, 0x00, 0x53, 0x00 ) .. - string.char( 0x63, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00) - - local socket = nmap.new_socket() - local try = nmap.new_try(catch) - - if(use_raw) then - try(socket:connect(host.ip,445,"tcp")) - else - try(socket:connect(host.ip,139,"tcp")) - end - - if(not use_raw) then - socket:set_timeout(100) - try(socket:send(rec1_payload)) - status, line1 = socket:receive_lines(1) - - if (not status) then - socket:close() - message = "Never received a response to SMB Session Request" - return 0, 0, message - end - end - - socket:set_timeout(100) - try(socket:send(rec2_payload)) - status, line2 = socket:receive_lines(1) - - if (not status) then - socket:close() - message = "Never received a response to SMB Negotiate Protocol Request" - return 0, 0, message - end - - currenttime, message = extract_time(line2); - - -- Check for an error parsing line2 - if(currenttime == 0) then - return 0, 0, message - end - - socket:set_timeout(100) - try(socket:send(rec3_payload)) - status, line3 = socket:receive_lines(1) - - if (not status) then - socket:close() - message = "Never received a response to SMB Setup AndX Request" - return 0, currenttime, message - end - socket:close() - - -- Check for an error parsing line3 - osversion, message = extract_version(line3) - if (osversion ~= 0) then - pos = string.find(osversion, win5) - if (pos ~= nil) then - osversion = "Windows 2000" - else - pos = string.find(osversion, win51) - if (pos ~= nil) then - osversion = "Windows XP" - end - end - end - - return osversion, currenttime, message - -end - ------------------------------------------------------------------------ --- Response from Session Setup AndX Request (TCP payload 3) --- Must be SMB response. Extract the OS version from it from a fixed --- offset in the payload. - -function extract_version(line) - - local temp, smb, ltemp, go, x, osversion, mychar, message - - smb = "SMB" .. string.char(0x73) - temp = string_concatenate(line, 6, 9) - message = 0 - - if (temp ~= smb) then - message = "Didn't find correct SMB record as a response to the Session Setup AndX request" - return 0, message - end - - ltemp = string.len(line) - temp = string_concatenate(line, 47, ltemp) - x=1 - - osversion = "" - while (x < ltemp) do - mychar = string.byte(temp,x) - if (mychar == 0) then - return osversion, message - else - osversion = osversion .. string.char(mychar) - end - x = x + 2 - end - - if (x >= ltemp) then - message = "OS version not found in expected record Session Setup AndX response" - return 0, message - end - -end - ------------------------------------------------------------------------ --- Response from Negotiate Protocol Response (TCP payload 2) --- Must be SMB response. Extract the time from it from a fixed --- offset in the payload. - -function extract_time(line) - - local smb, tmp, message, i, timebuf, timezonebuf, time, timezone - - message = 0 - - if(string.sub(line, 6, 8) ~= "SMB") then - message = "Didn't find correct SMB record as a response to the Negotiate Protocol Response" - return 0, message - end - - if(string.byte(line, 9) ~= 0x72) then - message = "Incorrect Negotiate Protocol Response type" - return 0, message - end - - -- Extract the timestamp from the response - i = 1 - time = 0 - timebuf = string.sub(line, 0x3d, 0x3d + 7) - while (i <= 8) do - time = time + 1.0 + (bit.lshift(string.byte(timebuf, i), 8 * (i - 1))) - i = i + 1 - end - -- Convert time from 1/10 microseconds to seconds - time = (time / 10000000) - 11644473600; - - -- Extract the timezone offset from the response - timezonebuf = string.sub(line, 0x45, 0x45 + 2) - timezone = (string.byte(timezonebuf, 1) + (bit.lshift(string.byte(timezonebuf, 2), 8))) - - -- This is a nasty little bit of code, so I'll explain it in detail. If the timezone has the - -- highest-order bit set, it means it was negative. If so, we want to take the two's complement - -- of it (not(x)+1) and divide by 60, to get minutes. Otherwise, just divide by 60. - -- To further complicate things (as if we needed _that_!), the timezone offset is the number of - -- minutes you'd have to add to the time to get to UTC, so it's actually the negative of what - -- we want. Confused yet? - if(timezone == 0x00) then - timezone = "UTC+0" - elseif(bit.band(timezone, 0x8000) == 0x8000) then - timezone = "UTC+" .. ((bit.band(bit.bnot(timezone), 0x0FFFF) + 1) / 60) - else - timezone = "UTC-" .. (timezone / 60) - end - - return (os.date("%Y-%m-%d %H:%M:%S", time) .. " " .. timezone), message; - -end - diff --git a/scripts/script.db b/scripts/script.db index 911e3439e..de7297ce0 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -1,94 +1,98 @@ -Entry{ category = "default", filename = "dns-test-open-recursion.nse" } -Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" } +Entry{ category = "default", filename = "showOwner.nse" } +Entry{ category = "safe", filename = "showOwner.nse" } +Entry{ category = "demo", filename = "daytimeTest.nse" } Entry{ category = "default", filename = "RealVNC_auth_bypass.nse" } Entry{ category = "malware", filename = "RealVNC_auth_bypass.nse" } Entry{ category = "vuln", filename = "RealVNC_auth_bypass.nse" } -Entry{ category = "external", filename = "dns-safe-recursion-port.nse" } -Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" } -Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" } -Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" } -Entry{ category = "default", filename = "showOwner.nse" } -Entry{ category = "safe", filename = "showOwner.nse" } -Entry{ category = "default", filename = "SSLv2-support.nse" } -Entry{ category = "safe", filename = "SSLv2-support.nse" } -Entry{ category = "malware", filename = "ircZombieTest.nse" } -Entry{ category = "version", filename = "skype_v2-version.nse" } -Entry{ category = "discovery", filename = "HTTPtrace.nse" } -Entry{ category = "demo", filename = "echoTest.nse" } -Entry{ category = "default", filename = "UPnP-info.nse" } -Entry{ category = "safe", filename = "UPnP-info.nse" } -Entry{ category = "default", filename = "rpcinfo.nse" } -Entry{ category = "safe", filename = "rpcinfo.nse" } -Entry{ category = "discovery", filename = "rpcinfo.nse" } -Entry{ category = "auth", filename = "bruteTelnet.nse" } -Entry{ category = "intrusive", filename = "bruteTelnet.nse" } -Entry{ category = "external", filename = "dns-safe-recursion-txid.nse" } -Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" } -Entry{ category = "default", filename = "SMTPcommands.nse" } -Entry{ category = "discovery", filename = "SMTPcommands.nse" } -Entry{ category = "safe", filename = "SMTPcommands.nse" } -Entry{ category = "default", filename = "robots.nse" } -Entry{ category = "safe", filename = "robots.nse" } -Entry{ category = "default", filename = "zoneTrans.nse" } -Entry{ category = "intrusive", filename = "zoneTrans.nse" } -Entry{ category = "discovery", filename = "zoneTrans.nse" } -Entry{ category = "discovery", filename = "whois.nse" } -Entry{ category = "external", filename = "whois.nse" } -Entry{ category = "safe", filename = "whois.nse" } -Entry{ category = "discovery", filename = "ripeQuery.nse" } -Entry{ category = "external", filename = "ripeQuery.nse" } -Entry{ category = "demo", filename = "chargenTest.nse" } -Entry{ category = "malware", filename = "strangeSMTPport.nse" } -Entry{ category = "version", filename = "iax2Detect.nse" } -Entry{ category = "demo", filename = "showSMTPVersion.nse" } -Entry{ category = "discovery", filename = "ASN.nse" } -Entry{ category = "external", filename = "ASN.nse" } -Entry{ category = "default", filename = "showHTMLTitle.nse" } -Entry{ category = "demo", filename = "showHTMLTitle.nse" } -Entry{ category = "safe", filename = "showHTMLTitle.nse" } -Entry{ category = "discovery", filename = "promiscuous.nse" } -Entry{ category = "version", filename = "netbios-smb-os-discovery.nse" } -Entry{ category = "default", filename = "anonFTP.nse" } -Entry{ category = "auth", filename = "anonFTP.nse" } -Entry{ category = "intrusive", filename = "anonFTP.nse" } Entry{ category = "intrusive", filename = "SQLInject.nse" } Entry{ category = "vuln", filename = "SQLInject.nse" } +Entry{ category = "auth", filename = "bruteTelnet.nse" } +Entry{ category = "intrusive", filename = "bruteTelnet.nse" } +Entry{ category = "discovery", filename = "HTTPtrace.nse" } Entry{ category = "demo", filename = "SMTP_openrelay_test.nse" } -Entry{ category = "default", filename = "nbstat.nse" } -Entry{ category = "discovery", filename = "nbstat.nse" } -Entry{ category = "safe", filename = "nbstat.nse" } Entry{ category = "default", filename = "HTTPAuth.nse" } Entry{ category = "auth", filename = "HTTPAuth.nse" } Entry{ category = "intrusive", filename = "HTTPAuth.nse" } -Entry{ category = "default", filename = "finger.nse" } -Entry{ category = "discovery", filename = "finger.nse" } -Entry{ category = "demo", filename = "showHTTPVersion.nse" } -Entry{ category = "default", filename = "SSHv1-support.nse" } -Entry{ category = "safe", filename = "SSHv1-support.nse" } -Entry{ category = "default", filename = "popcapa.nse" } -Entry{ category = "default", filename = "SNMPsysdescr.nse" } -Entry{ category = "discovery", filename = "SNMPsysdescr.nse" } -Entry{ category = "safe", filename = "SNMPsysdescr.nse" } -Entry{ category = "intrusive", filename = "brutePOP3.nse" } -Entry{ category = "auth", filename = "brutePOP3.nse" } -Entry{ category = "default", filename = "MySQLinfo.nse" } -Entry{ category = "discovery", filename = "MySQLinfo.nse" } -Entry{ category = "safe", filename = "MySQLinfo.nse" } -Entry{ category = "default", filename = "ftpbounce.nse" } -Entry{ category = "intrusive", filename = "ftpbounce.nse" } -Entry{ category = "auth", filename = "xamppDefaultPass.nse" } -Entry{ category = "vuln", filename = "xamppDefaultPass.nse" } -Entry{ category = "intrusive", filename = "HTTPpasswd.nse" } -Entry{ category = "vuln", filename = "HTTPpasswd.nse" } -Entry{ category = "demo", filename = "showSSHVersion.nse" } -Entry{ category = "version", filename = "PPTPversion.nse" } -Entry{ category = "default", filename = "ircServerInfo.nse" } -Entry{ category = "discovery", filename = "ircServerInfo.nse" } +Entry{ category = "default", filename = "dns-test-open-recursion.nse" } +Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" } +Entry{ category = "demo", filename = "chargenTest.nse" } +Entry{ category = "default", filename = "showHTMLTitle.nse" } +Entry{ category = "demo", filename = "showHTMLTitle.nse" } +Entry{ category = "safe", filename = "showHTMLTitle.nse" } Entry{ category = "default", filename = "MSSQLm.nse" } Entry{ category = "discovery", filename = "MSSQLm.nse" } Entry{ category = "intrusive", filename = "MSSQLm.nse" } +Entry{ category = "demo", filename = "echoTest.nse" } +Entry{ category = "default", filename = "SSHv1-support.nse" } +Entry{ category = "safe", filename = "SSHv1-support.nse" } +Entry{ category = "default", filename = "MySQLinfo.nse" } +Entry{ category = "discovery", filename = "MySQLinfo.nse" } +Entry{ category = "safe", filename = "MySQLinfo.nse" } +Entry{ category = "auth", filename = "xamppDefaultPass.nse" } +Entry{ category = "vuln", filename = "xamppDefaultPass.nse" } +Entry{ category = "default", filename = "SSLv2-support.nse" } +Entry{ category = "safe", filename = "SSLv2-support.nse" } +Entry{ category = "default", filename = "zoneTrans.nse" } +Entry{ category = "intrusive", filename = "zoneTrans.nse" } +Entry{ category = "discovery", filename = "zoneTrans.nse" } +Entry{ category = "default", filename = "ftpbounce.nse" } +Entry{ category = "intrusive", filename = "ftpbounce.nse" } +Entry{ category = "version", filename = "skype_v2-version.nse" } +Entry{ category = "discovery", filename = "promiscuous.nse" } +Entry{ category = "default", filename = "SNMPsysdescr.nse" } +Entry{ category = "discovery", filename = "SNMPsysdescr.nse" } +Entry{ category = "safe", filename = "SNMPsysdescr.nse" } +Entry{ category = "demo", filename = "showSMTPVersion.nse" } +Entry{ category = "default", filename = "nbstat.nse" } +Entry{ category = "discovery", filename = "nbstat.nse" } +Entry{ category = "safe", filename = "nbstat.nse" } +Entry{ category = "version", filename = "iax2Detect.nse" } +Entry{ category = "default", filename = "rpcinfo.nse" } +Entry{ category = "safe", filename = "rpcinfo.nse" } +Entry{ category = "discovery", filename = "rpcinfo.nse" } Entry{ category = "default", filename = "HTTP_open_proxy.nse" } Entry{ category = "discovery", filename = "HTTP_open_proxy.nse" } Entry{ category = "external", filename = "HTTP_open_proxy.nse" } Entry{ category = "intrusive", filename = "HTTP_open_proxy.nse" } -Entry{ category = "demo", filename = "daytimeTest.nse" } +Entry{ category = "intrusive", filename = "HTTPpasswd.nse" } +Entry{ category = "vuln", filename = "HTTPpasswd.nse" } +Entry{ category = "demo", filename = "showSSHVersion.nse" } +Entry{ category = "default", filename = "SMTPcommands.nse" } +Entry{ category = "discovery", filename = "SMTPcommands.nse" } +Entry{ category = "safe", filename = "SMTPcommands.nse" } +Entry{ category = "default", filename = "anonFTP.nse" } +Entry{ category = "auth", filename = "anonFTP.nse" } +Entry{ category = "intrusive", filename = "anonFTP.nse" } +Entry{ category = "default", filename = "robots.nse" } +Entry{ category = "safe", filename = "robots.nse" } +Entry{ category = "default", filename = "finger.nse" } +Entry{ category = "discovery", filename = "finger.nse" } +Entry{ category = "default", filename = "UPnP-info.nse" } +Entry{ category = "safe", filename = "UPnP-info.nse" } +Entry{ category = "malware", filename = "strangeSMTPport.nse" } +Entry{ category = "default", filename = "ircServerInfo.nse" } +Entry{ category = "discovery", filename = "ircServerInfo.nse" } +Entry{ category = "malware", filename = "ircZombieTest.nse" } +Entry{ category = "discovery", filename = "ripeQuery.nse" } +Entry{ category = "external", filename = "ripeQuery.nse" } +Entry{ category = "demo", filename = "showHTTPVersion.nse" } +Entry{ category = "version", filename = "PPTPversion.nse" } +Entry{ category = "discovery", filename = "ASN.nse" } +Entry{ category = "external", filename = "ASN.nse" } +Entry{ category = "intrusive", filename = "brutePOP3.nse" } +Entry{ category = "auth", filename = "brutePOP3.nse" } +Entry{ category = "default", filename = "popcapa.nse" } +Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" } +Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" } +Entry{ category = "discovery", filename = "whois.nse" } +Entry{ category = "external", filename = "whois.nse" } +Entry{ category = "safe", filename = "whois.nse" } +Entry{ category = "external", filename = "dns-safe-recursion-txid.nse" } +Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" } +Entry{ category = "version", filename = "smb-enum.nse" } +Entry{ category = "intrusive", filename = "smb-enum.nse" } +Entry{ category = "external", filename = "dns-safe-recursion-port.nse" } +Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" } +Entry{ category = "version", filename = "smb-os-discovery.nse" } +Entry{ category = "default", filename = "smb-os-discovery.nse" } +Entry{ category = "version", filename = "smb-security-mode.nse" } diff --git a/scripts/smb-enum.nse b/scripts/smb-enum.nse new file mode 100644 index 000000000..0552ebb79 --- /dev/null +++ b/scripts/smb-enum.nse @@ -0,0 +1,197 @@ +--- Attempts to enumerate users and shares anonymously over SMB. +-- +-- First, it logs in as the anonymous user and tries to connect to IPC$. +-- If it is successful, it knows that Null sessions are enabled. If it +-- is unsuccessful, it can still check for shares (because Windows is +-- cool like that). A list of common shares is checked (see the 'shares' +-- variable) to see what anonymous can access. Either a successful result +-- is returned (has access), STATUS_ACCESS_DENIED is returned (exists but +-- anonymous can't access), or STATUS_BAD_NETWORK_NAME is returned (doesn't +-- exist). +-- +-- Next, the Guest account is attempted with a blank password. If it's +-- enabled, a message is displayed and shares that it has access to are +-- checked the same as anonymous. +-- +-- Finally, the Administrator account is attempted with a blank password. +-- Because Administrator can't typically be locked out, this should be +-- safe. That being said, it is possible to configure Administrator to +-- be lockoutable, so watch out for that caveat. If you do lock yourself +-- out of Administrator, there's a bootdisk that can help. :) +-- +-- If Administrator has a blank password, it often doesn't allow remote +-- logins, if this is the case, STATUS_ACCOUNT_RESTRICTION is returned +-- instead of STATUS_ACCESS_DENIED, so we know the account has no password. +-- +--@usage +-- nmap --script smb-enum.nse -p445 127.0.0.1\n +-- sudo nmap -sU -sS --script smb-enum.nse -p U:137,T:139 127.0.0.1\n +-- +--@output +-- Host script results: +-- | SMB Enumeration: +-- | Null sessions enabled +-- | Anonymous shares found: IPC$ +-- | Restricted shares found: C$ TEST +-- | Guest account is enabled +-- | Guest can access: IPC$ TEST +-- | Administrator account has a blank password +-- |_ Administrator can access: IPC$ C$ TEST +----------------------------------------------------------------------- + +id = "SMB Enumeration" +description = "Attempts to enumerate users and shares anonymously over SMB" +author = "Ron Bowes" +copyright = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"version","intrusive"} + +require 'smb' + +-- Shares to try connecting to as Null session / GUEST +local shares = {"IPC", "C", "D", "TEST", "SHARE", "HOME", "DFS", "COMCFG" } + +hostrule = function(host) + + local port = smb.get_port(host) + + if(port == nil) then + return false + else + return true + end + +end +--- Attempts to connect to a list of shares as the given UID, returning the +-- shares that it has and doesn't have access to. +--@param socket The socket to use +--@param ip The ip address of the host +--@param uid The UserID we're logged in as +--@return (allowed_shares, denied_shares) Lists of shares we can and can't access, +-- but all of which exist. +function find_shares(socket, ip, uid) + local i + local allowed_shares = {} + local denied_shares = {} + + + for i = 1, #shares, 1 do + + local share = string.format("\\\\%s\\%s", ip, shares[i]) + + status, tree_result = smb.tree_connect(socket, share, uid) + if(status == false) then + if(tree_result == 0xc0000022) then -- STATUS_ACCESS_DENIED + denied_shares[#denied_shares + 1] = shares[i] + end + else + allowed_shares[#allowed_shares + 1] = shares[i] + end + + share = share .. "$" + status, tree_result = smb.tree_connect(socket, share, uid) + if(status == false) then + if(tree_result == 0xc0000022) then -- STATUS_ACCESS_DENIED + denied_shares[#denied_shares + 1] = shares[i] .. "$" + end + else + allowed_shares[#allowed_shares + 1] = shares[i] .. "$" + end + + end + + return allowed_shares, denied_shares +end + +--- Join strings together with a space. +function string_join(table) + local i + local response = " " + + for i = 1, #table, 1 do + response = response .. table[i] .. " " + end + + return response +end + +action = function(host) + local response = " \n" + local status, socket, negotiate_result, session_result + local allowed_shares, restricted_shares + + status, socket = smb.start(host) + if(status == false) then + return "ERROR: " .. socket + end + + status, negotiate_result = smb.negotiate_protocol(socket) + if(status == false) then + smb.stop(socket) + return "ERROR: " .. negotiate_result + end + + -- Start up a null session + status, session_result = smb.start_session(socket, "", negotiate_result['session_key'], negotiate_result['capabilities']) + if(status == false) then + smb.stop(socket) + return "ERROR: " .. session_result + end + + -- Check if null session has access to IPC$ + status, result = smb.tree_connect(socket, "IPC$", session_result['uid']) + if(status == true) then + response = response .. "Null sessions enabled\n" + end + + -- Find shares + allowed_shares, restricted_shares = find_shares(socket, host.ip, session_result['uid']) + + -- Display shares the Null user had access to + if(#allowed_shares > 0) then + response = response .. "Anonymous shares found: " .. string_join(allowed_shares) .. "\n" + end + + -- Display shares the Null user didn't have access to + if(#restricted_shares > 0) then + response = response .. "Restricted shares found: " .. string_join(restricted_shares) .. "\n" + end + + -- Check if Guest can log in + status, session_result = smb.start_session(socket, "GUEST", negotiate_result['session_key'], negotiate_result['capabilities']) + if(status == true) then + response = response .. "Guest account is enabled\n" + + -- Find shares for Guest + allowed_shares, restricted_shares = find_shares(socket, host.ip, session_result['uid']) + + -- Display shares Guest had access to + if(#allowed_shares > 0) then + response = response .. "Guest can access: " .. string_join(allowed_shares) .. "\n" + end + end + + -- Check if Administrator has a blank password + -- (we check Administrator and not other accounts because Administrator can't generally be locked out) + status, session_result = smb.start_session(socket, "ADMINISTRATOR", negotiate_result['session_key'], negotiate_result['capabilities']) + if(status == true) then + response = response .. "Administrator account has a blank password\n" + + -- Find shares for Administrator + allowed_shares, restricted_shares = find_shares(socket, host.ip, session_result['uid']) + + -- Display shares administrator had access to + if(#allowed_shares > 0) then + response = response .. "Administrator can access: " .. string_join(allowed_shares) .. "\n" + end + elseif(session_result == 0xc000006e) then -- STATUS_ACCOUNT_RESTRICTION + response = response .. "Administrator account has a blank password, but can't use SMB\n" + end + + + + smb.stop(socket) + return response +end + + diff --git a/scripts/smb-os-discovery.nse b/scripts/smb-os-discovery.nse new file mode 100644 index 000000000..8ba695d6c --- /dev/null +++ b/scripts/smb-os-discovery.nse @@ -0,0 +1,80 @@ +--- Attempts to determine the operating system over SMB protocol (ports 445 and 139). +-- See nselib/smb.lua for more information on this protocol. +-- +--@usage +-- nmap --script smb-os-discovery.nse -p445 127.0.0.1\n +-- sudo nmap -sU -sS --script smb-os-discovery.nse -p U:137,T:139 127.0.0.1\n +-- +--@output +-- | OS from SMB: Windows 2000 +-- | LAN Manager: Windows 2000 LAN Manager +-- | Name: WORKGROUP\TEST1 +-- |_ System time: 2008-09-09 20:55:55 UTC-5 +-- +----------------------------------------------------------------------- + +id = "OS from SMB" +description = "Attempts to determine the operating system over the SMB protocol (ports 445 and 139)." +author = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"version","default"} + +require 'smb' +require 'stdnse' + +--- Check whether or not this script should be run. +hostrule = function(host) + + local port = smb.get_port(host) + + if(port == nil) then + return false + else + return true + end + +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) + + status, socket = smb.start(host) + + if(status == false) then + return "Error: " .. socket + end + + status, negotiate_result = smb.negotiate_protocol(socket) + + if(status == false) then + stdnse.print_debug(2, "Negotiate session failed") + smb.stop(socket) + return "Error: " .. negotiate_result + end + + status, session_result = smb.start_session(socket, "", negotiate_result['session_key'], negotiate_result['capabilities']) + + if(status == false) then + smb.stop(socket) + return "Error: " .. session_result + end + + smb.stop(socket) + return string.format("%s\nLAN Manager: %s\nName: %s\\%s\nSystem time: %s %s\n", get_windows_version(session_result['os']), session_result['lanmanager'], negotiate_result['domain'], negotiate_result['server'], negotiate_result['date'], negotiate_result['timezone_str']) +end + + diff --git a/scripts/smb-security-mode.nse b/scripts/smb-security-mode.nse new file mode 100644 index 000000000..83fddba3b --- /dev/null +++ b/scripts/smb-security-mode.nse @@ -0,0 +1,112 @@ +--- Returns information about the SMB security level determined by SMB. +-- +-- Here is how to interpret the output: +-- +-- User-level security: Each user has a separate username/password that is used +-- to log into the system. This is the default setup of pretty much everything +-- these days. +-- Share-level security: The anonymous account should be used to log in, then +-- the password is given (in plaintext) when a share is accessed. All users who +-- have access to the share use this password. This was the original way of doing +-- things, but isn't commonly seen, now. If a server uses share-level security, +-- it is vulnerable to sniffing. +-- +-- Challenge/response passwords: If enabled, the server can accept any type of +-- password: +-- * Plaintext +-- * LM and NTLM +-- * LMv2 and NTLMv2 +-- If it isn't set, the server can only accept plaintext passwords. Most servers +-- are configured to use challenge/response these days. If a server is configured +-- to accept plaintext passwords, it is vulnerable to sniffing. +-- +-- Message signing: If required, all messages between the client and server must +-- sign be signed by a shared key, derived from the password and the server +-- challenge. If supported and not required, message signing is negotiated between +-- clients and servers and used if both support and request it. By default, Windows clients +-- don't sign messages, so if message signing isn't required by the server, messages +-- probably won't be signed; additionally, if performing a man-in-the-middle attack, +-- an attacker can negotiate no message signing. If message signing isn't required, the +-- server is vulnerable to man-in-the-middle attacks. +-- +-- See nselib/smb.lua for more information on the protocol itself. +-- +--@usage +-- nmap --script smb-security-mide.nse -p445 127.0.0.1\n +-- sudo nmap -sU -sS --script smb-security-mide.nse -p U:137,T:139 127.0.0.1\n +-- +--@output +-- | SMB Security: User-level authentication +-- | SMB Security: Challenge/response passwords supported +-- |_ SMB Security: Message signing supported +-- +----------------------------------------------------------------------- + +id = "SMB Security" +description = "Attempts to determine the security mode over the SMB protocol (ports 445 and 139)." +author = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"version"} + +require 'smb' + +--- Check whether or not this script should be run. +hostrule = function(host) + + local port = smb.get_port(host) + + if(port == nil) then + return false + else + return true + end + +end + + +action = function(host) + + local status, socket = smb.start(host) + + if(status == false) then + return "Error: " .. socket + end + + status, result = smb.negotiate_protocol(socket) + + if(status == false) then + smb.stop(socket) + return "Error: " .. result + end + + local security_mode = result['security_mode'] + local response = "" + + -- User-level authentication or share-level authentication + if(bit.band(security_mode, 1) == 1) then + response = response .. "User-level authentication\n" + else + response = response .. " 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 + + smb.stop(socket) + return response +end + +