|
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
By Date
By Thread
Current thread:
- [NSE RFC] SMB Probe Ron (Sep 07)
|