Nmap Development mailing list archives

[NSE RFC] SMB Probe


From: Ron <ron () skullsecurity net>
Date: Sun, 07 Sep 2008 13:27:33 -0500

Hey all,

I spent the weekend writing a new SMB script. It's designed to replace
"netbios-smb-os-discovery.nse" and can also replace "nbstat.nse" fairly
easily, since by necessity it duplicates the functionality.

I used nsedoc comments throughout, including a lengthy description
field, so I won't repeat myself here. Check out the first bit for a
bunch of information. Since I don't have a nsedoc parse, I might be
doing it totally wrong, so let me know.

I'm interested in comments on the style and such. I'm new to Lua, but
I've been picking it up. There may be things about it I don't know, and
I'm interested in learning. For example, I only just realized that
'local' is important, after debugging something nasty!

The other bit I'm unsure about is the output. Right now, it builds the
string as it goes along, but I might change it to build an array of
strings instead. It's also a little chatty at the moment, although I
think everything it displays is important. I might up the verbosity on
some of it, though.

Anyways, this works well against all my test boxes, and I kept it pretty
clean (using pack/unpack to build packets, for example). I plan to
expand this far more in the future, this is just the basic stuff. I'd
appreciate output, though, and I hope to get a version done soon that
can be included.

Anyways, here are some examples......

--
Against a default Win2k box, over SMB
--
$ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p445
192.168.1.41

Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:22 CDT
Interesting ports on 192.168.1.41:
PORT    STATE SERVICE
445/tcp open  microsoft-ds

Host script results:
|  Probe SMB for information: (using port 445):
|  SMB Security: User-level authentication
|  SMB Security: Challenge/response passwords supported
|  SMB Security: Message signing supported
|  System time from SMB: 2008-09-07 00:59:47 [UTC-5]
|  Computer name from SMB: WORKGROUP\TEST1
|  OS detection from SMB: Windows 2000
|  Null sessions enabled
|  Found a share 'TEST'
|  Found a share 'TEST$'
|  Guest account enabled
|_ Guest account has access to share 'TEST'

Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds
--

--
Against a locked down Win2k box, over NetBIOS
--
$ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139
192.168.1.42

Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:13 CDT
Interesting ports on 192.168.1.42:
PORT    STATE SERVICE
139/tcp open  netbios-ssn

Host script results:
|  Probe SMB for information: (using port 139):
|  SMB Security: User-level authentication
|  SMB Security: Challenge/response passwords supported
|  SMB Security: Message signing required
|  System time from SMB: 2008-09-07 13:13:41 [UTC-5]
|  Computer name from SMB: WORKGROUP\TEST2
|  OS detection from SMB: Windows 2000
|  Null sessions disabled
|_ Guest account disabled

Nmap done: 1 IP address (1 host up) scanned in 0.17 seconds
--

--
Against Windows XP
--
$ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139
192.168.1.44

Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:18 CDT
Interesting ports on 192.168.1.44:
PORT    STATE SERVICE
139/tcp open  netbios-ssn

Host script results:
|  Probe SMB for information: (using port 139):
|  SMB Security: User-level authentication
|  SMB Security: Challenge/response passwords supported
|  SMB Security: Message signing not supported
|  System time from SMB: 2008-09-07 13:18:58 [UTC-5]
|  Computer name from SMB: WORKGROUP\DF
|  OS detection from SMB: Windows XP
|  Null sessions enabled
|_ Guest account disabled

Nmap done: 1 IP address (1 host up) scanned in 1.06 seconds
--

--
Against Windows Server 2003 (with a blank admin password, apparently)
--
$ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p139
192.168.1.45

Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:20 CDT
Interesting ports on 192.168.1.45:
PORT    STATE SERVICE
139/tcp open  netbios-ssn

Host script results:
|  Probe SMB for information: (using port 139):
|  SMB Security: User-level authentication
|  SMB Security: Challenge/response passwords supported
|  SMB Security: Message signing not supported
|  System time from SMB: 2008-08-23 10:17:17 [UTC-5]
|  Computer name from SMB: WORKGROUP\RON-PENTEST
|  OS detection from SMB: Windows Server 2003 3790 Service Pack 2
|  Null sessions enabled
|  Guest account disabled
|_ Administrator account has a blank password (but can't use SMB)

Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds
--

--
Against Windows Vista
--
$ ./nmap --script /home/ron/tmp/nmap/scripts/smb-probe.nse -p445
192.168.2.123

Starting Nmap 4.69BETA1 ( http://nmap.org ) at 2008-09-07 13:23 CDT
Interesting ports on 192.168.2.123:
PORT    STATE SERVICE
445/tcp open  microsoft-ds

Host script results:
|  Probe SMB for information: (using port 445):
|  SMB Security: User-level authentication
|  SMB Security: Challenge/response passwords supported
|  SMB Security: Message signing not supported
|  System time from SMB: 2008-09-07 10:57:20 [UTC-7]
|  Computer name from SMB: WORKGROUP\RON-PC
|  OS detection from SMB: Windows Vista (TM) Ultimate 6000
|  Null sessions enabled
|_ Guest account disabled

Nmap done: 1 IP address (1 host up) scanned in 1.70 seconds
--
--- Probes a system running SMB (CIFS) for more information. 
--
-- First, a connection to SMB has to be made. This script will try two 
-- ways (in this order):\n
-- 1) Attempt to start a raw session over 445, if it's open. \n
-- 2) Attempt to start a NetBIOS session over 139. Although the 
--    protocol's the same, it requires a "session request" packet. 
--    That packet requires the computer's name, which is requested
--    using a NBSTAT probe over UDP port 137. \n
--
-- Once it's connected, a SMB_COM_NEGOTIATE_PROTOCOL packet is sent, 
-- requesting the protocol "NT LM 0.12", which is the most commonly
-- supported one. Among other things, the server's response contains
-- the host's security level, the system time, and the computer/domain
-- name. The security level is whether or not the server supports 
-- challenge/response passwords, and whether or not it supports/requires
-- message signatures (if they aren't required, a host is more vulnerable 
-- to man-in-the-middle attacks). 
--
-- If that's successful, it next sends a series of SMB_COM_SESSION_START_ANDX
-- requests. It attempts to start sessions as the following users:\n
-- 1) [blank] (Null session)\n
-- 2) Guest\n
-- 3) Administrator\n
-- All with blank passwords. Typically, if the administrator account has
-- a blank password, it can't be used over SMB. However, a different error
-- code is returned if it's a blank password compared to the wrong password,
-- so we make note. 
--
-- Another point of interest in the response to SMB_COM_SESSION_START_ANDX
-- is the system's OS, so we make note of that, as well (thanks to Judy Novak
-- and Sourcefire Inc for the idea, in the original SMB script). 
--
-- After a successful SMB_COM_SESSION_START_ANDX for a Null session or guest,
-- a bunch of SMB_COM_TREE_CONNECT_ANDX requests are sent out. There is an 
-- array of shares to try and access called 'shares', and each of them is tried
-- with and without a trailing dollar sign ('$'). The shares currently included
-- are:\n
-- * C\n
-- * D\n
-- * TEST\n
-- * SHARE\n
-- * HOME\n
-- There is absolutely no reason I picked those, they seemed like they'd be
-- common. The IPC$ share is also checked, but since everybody has access to
-- it, it's only used to determine whether or not Null sessions are disabled. 
--
-- Each share will either return STATUS_BAD_NETWORK_NAME if the share doesn't
-- exist, STATUS_ACCESS_DENIED if it exists but we don't have access, or 
-- STATUS_SUCCESS if exists and we do have access. 
--
-- Thanks go to Christopher R. Hertel and Implementing CIFS, which 
-- taught me everything I know about Microsoft's protocols. 
--
--@usage
-- nmap --script smb-probe.nse -p445 127.0.0.1\n
-- sudo nmap -sU -sS --script smb-probe.nse -p U:137,T:139 127.0.0.1\n
--
--@output
-- Against a weak box:
-- Host script results:\n
-- |  Probe SMB for information: \n
-- |  SMB Security: User-level authentication\n
-- |  SMB Security: Challenge/response passwords supported\n
-- |  SMB Security: Message signing supported\n
-- |  System time from SMB: 2008-09-07 00:56:02 [UTC-5]\n
-- |  Computer name from SMB: WORKGROUP\TEST1\n
-- |  OS detection from SMB: Windows 2000\n
-- |  Null sessions enabled\n
-- |  Found a share 'TEST'\n
-- |  Found a share 'TEST$'\n
-- |  Guest account enabled\n
-- |_ Guest account has access to share 'TEST'\n
--\n
-- Against a more locked down host:\n
-- Host script results:\n
-- |  Probe SMB for information:  \n
-- |  SMB Security: User-level authentication\n
-- |  SMB Security: Challenge/response passwords supported\n
-- |  SMB Security: Message signing required\n
-- |  System time from SMB: 2008-09-07 01:55:46 [UTC-5]\n
-- |  Computer name from SMB: WORKGROUP\TEST2\n
-- |  OS detection from SMB: Windows 2000\n
-- |  Null sessions disabled\n
-- |_ Guest account disabled\n
-- 

-----------------------------------------------------------------------

id = "Probe SMB for information"
description = "Elicits information from a host running NetBIOS/SMB"
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html";
categories = {"version","intrusive"}

require 'bit'
require 'bin'

-- The address being scanned
local address

-- The key for the session, which is simply echoed back
local session_key

-- The capabilities for the session, which are echoed back
local capabilities

-- The "encryption key", aka, the server challenge
local server_challenge

-- Set to the most recent user logged in (from SMB_COM_SESSION_SETUP_ANDX)
local uid = 0

-- Set to the most recent tree connected to (from SMB_COM_TREE_CONNECT_ANDX)
local tid = 0

-- Shares to try connecting to as Null session / GUEST
local shares = {"C", "D", "TEST", "SHARE", "HOME"}

--- Check whether or not this script should be fun. This script should 
--  run under two different conditions:
-- a) port tcp/445 is open (allowing us to make a raw connection)\n
-- b) ports tcp/139 and udp/137 are open (137 may not be known)\n
hostrule = function(host)


        local port_u137 = nmap.get_port_state(host, {number=137, protocol="udp"})
        local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"})
        local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})

        if(port_t445 ~= nil and port_t445.state == "open") then
                 -- tcp/445 is open, we're good
                 return true
        end

        if(port_t139 ~= nil and port_t139.state == "open") then
                 -- tcp/139 is open, check uf udp/137 is open or unknown
                 if(port_u137 == nil or port_u137.state == "open" or port_u137.state == "open|filtered") then
                          return true
                 end
        end

        return false
end

--- Encode a NetBIOS name for transport. Most packets that use the NetBIOS name
--  require this encoding to happen first. It takes a name containing any possible
--  character, and converted it to all uppercase characters (so it can, for example,
--  pass case-sensitive data in a case-insensitive way)
--
-- There are two levels of encoding performed:\n
-- L1: Pad the string to 16 characters withs spaces (or NULLs if it's the 
--     wildcard "*") and replace each byte with two bytes representing each
--     of its nibbles, plus 0x41. \n
-- L2: Prepend the length to the string, and to each substring in the scope
--     (separated by periods). \n
--@param name The name that will be encoded (eg. "TEST1"). 
--@param scope [optional] The scope to encode it with. I've never seen scopes used
--       in the real world (eg, "insecure.org"). 
--@return The L2-encoded name and scope 
--        (eg. "\x20FEEFFDFEDBCACACACACACACACACAAA\x08insecure\x03org")
function netbios_name_encode(name, scope)
        -- Truncate or pad the string to 16 bytes
        if(string.len(name) > 16) then
                name = string.sub(name, 1, 16)
        else
                local padding = " "
                if name == "*" then
                        padding = "\0"
                end

                repeat
                        name = name .. padding
                until string.len(name) == 16
        end

        -- Do the L1 encoding
        local L1_encoded = ""
        for i=1, string.len(name), 1 do
                local b = string.byte(name, i)
                L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0xF0), 4) + 0x41)
                L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0x0F), 0) + 0x41)
        end

        -- Do the L2 encoding 
        local L2_encoded = string.char(32) .. L1_encoded

        if scope ~= nil then
                -- Split the scope at its periods
                local piece
                for piece in string.gmatch(scope, "[^.]+") do
                        L2_encoded = L2_encoded .. string.char(string.len(piece)) .. piece
                end
        end

        return L2_encoded
end



--- Does the exact opposite of netbios_name_encode. Converts an encoded name to
--  the string representation. If the encoding is invalid, it will still attempt
--  to decode the string as best as possible. 
--@param encoded_name The L2-encoded name
--@returns the decoded name and the scope. The name will still be padded, and the
--         scope will never be nil (empty string is returned if no scope is present)
function netbios_name_decode(encoded_name)
        local name = ""
        local scope = ""

        local len = string.byte(encoded_name, 1)
        local i

        for i = 2, len + 1, 2 do
                local ch = 0
                ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i)     - 0x41, 4))
                ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i + 1) - 0x41, 0))

                name = name .. string.char(ch)
        end

        -- Decode the scope
        local pos = 34
        while string.len(encoded_name) > pos do
                local len = string.byte(encoded_name, pos)
                scope = scope .. string.sub(encoded_name, pos + 1, pos + len) .. "."
                pos = pos + 1 + len
        end

        -- If there was a scope, remove the trailing period
        if(string.len(scope) > 0) then
                scope = string.sub(scope, 1, string.len(scope) - 1)
        end

        return name, scope
end

--- Sends out a UDP probe on port 137 to get a human-readable list of names the
--  the system is using. 
--@param host The IP or hostname to check. 
--@param prefix [optional] The prefix to put on each line when it's returned. 
--@return (status, result) If status is true, the result is a human-readable 
--        list of names. Otherwise, result is an error message. 
function netbios_get_names(host, prefix)

        local status, names, name_count = netbios_do_nbstat(host)

        if(prefix == nil) then
                prefix = ""
        end


        if(status) then
                local result = ""
                for i = 1, name_count, 1 do
                        result = result .. string.format("%s%s<%02x>\n", prefix, names[i][1], names[i][2])
                end

                return true, result
        else
                return false, names
        end
end

--- Sends out a UDP probe on port 137 to get the server's name (that is, the
--  entry in its NBSTAT table with a 0x20 suffix). 
--@param host The IP or hostname of the server. 
--@return (status, result) If status is true, the result is the NetBIOS name. 
--        otherwise, result is an error message. 
function netbios_get_server_name(host)
        local status, names, name_count = netbios_do_nbstat(host)

        if(status) then
                local i
                for i = 1, name_count, 1 do
                        if names[i][2] == 0x20 then
                                return true, names[i][1]
                        end
                end
        else
                return false, names
        end

        return false, "Couldn't find NetBIOS server name"
end


--- This is the function that actually handles the UDP query to retrieve
--  the NBSTAT information. 
--
-- The NetBIOS request's header looks like this:
--  --------------------------------------------------\n
--  |  15 14 13 12 11 10 9  8  7  6  5  4  3  2  1  0 |\n
--  |                  NAME_TRN_ID                    |\n
--  | R |   OPCODE  |      NM_FLAGS      |   RCODE    | (FLAGS)\n
--  |                    QDCOUNT                      |\n
--  |                    ANCOUNT                      |\n
--  |                    NSCOUNT                      |\n
--  |                    ARCOUNT                      |\n
--  --------------------------------------------------\n
--
-- In this case, the TRN_ID is a constant (0x1337, what else?), the flags
-- are 0, and we have one question. All fields are network byte order. 
--
-- The body of the packet is a list of names to check for in the following
-- format:
-- (ntstring) encoded name
-- (2 bytes)  query type (0x0021 = NBSTAT)
-- (2 bytes)  query class (0x0001 = IN)
--
-- The response header is the exact same, except it'll have some flags set
-- (0x8000 for sure, since it's a response), and ANCOUNT will be 1. The format
-- of the answer is:\n
-- (ntstring) requested name\n
-- (2 bytes)  query type\n
-- (2 bytes)  query class\n
-- (2 bytes)  time to live\n
-- (2 bytes)  record length\n
-- (1 byte)   number of names\n
-- [for each name]\n
--  (16 bytes) padded name, with a 1-byte suffix\n
--  (2 bytes)  flags\n
--
--@param host The IP or hostname of the system. 
--@return (status, result) If status is true, then the servers names are
--        returned as an array of (name, suffix, flags). 
--        Otherwise, result is an error message. 
function netbios_do_nbstat(host)

        local socket = nmap.new_socket()
        local encoded_name = netbios_name_encode("*")

        -- Create the query header
        local query = bin.pack(">SSSSSS", 
                        0x1337,  -- Transaction id
                        0x0000,  -- Flags
                        1,       -- Questions
                        0,       -- Answers
                        0,       -- Authority
                        0        -- Extra
                )

        query = query .. bin.pack(">zSS", 
                        encoded_name, -- Encoded name
                        0x0021,       -- Query type (0x21 = NBSTAT)
                        0x0001        -- Class = IN
                )
        socket:connect(host, 137, "udp")
        socket:send(query)
        socket:set_timeout(1000)

        local status, result = socket:receive_bytes(1);
        socket:close()

        if(status) then
                local pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, rr_name, rr_type, rr_class, rr_ttl
                local rrlength, name_count

                pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT = bin.unpack(">SSSSSS", result)

                -- Sanity check the result (has to have the same TRN_ID, 1 answer, and proper flags)
                if(TRN_ID ~= 0x1337) then
                        return false, string.format("Invalid transaction ID returned: 0x%04x", TRN_ID)
                end
                if(ANCOUNT ~= 1) then
                        return false, "Server returned an invalid number of answers"
                end
                if(bit.band(FLAGS, 0x8000) == 0) then
                        return false, "Server's flags didn't indicate a response"
                end
                if(bit.band(FLAGS, 0x0007) ~= 0) then
                        return false, string.format("Server returned a NetBIOS error: 0x%02x", bit.band(FLAGS, 0x0007))
                end

                -- Start parsing the answer field
                pos, rr_name, rr_type, rr_class, rr_ttl = bin.unpack(">zSSI", result, pos)

                -- More sanity checks
                if(rr_name ~= encoded_name) then
                        return false, "Server returned incorrect name"
                end
                if(rr_class ~= 0x0001) then
                        return false, "Server returned incorrect class"
                end
                if(rr_type ~= 0x0021) then
                        return false, "Server returned incorrect query type"
                end

                pos, rrlength, name_count = bin.unpack(">SC", result, pos)

                local names = {}
                for i = 1, name_count do
                        local name, suffix, flags

                        -- Instead of reading the 16-byte name and pulling off the suffix, 
                        -- we read the first 15 bytes and then the 1-byte suffix. 
                        pos, name, suffix, flags = bin.unpack(">A15CS", result, pos)
                        name = string.gsub(name, "[ ]*$", "")
                        names[i] = { name, suffix, flags }
                end
                
                return true, names, name_count

        else
                return false, "Name query failed: " .. result
        end
end

--- Creates a string containing a SMB packet header. The header looks like this:\n
-- --------------------------------------------------------------------------------------------------\n
-- | 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9  8  7  6  5  4  3  2  1  0 |\n
-- --------------------------------------------------------------------------------------------------\n
-- |         0xFF           |          'S'          |        'M'            |         'B'           |\n
-- --------------------------------------------------------------------------------------------------\n
-- |        Command         |                             Status...                                 |\n
-- --------------------------------------------------------------------------------------------------\n
-- |    ...Status           |        Flags          |                    Flags2                     |\n
-- --------------------------------------------------------------------------------------------------\n
-- |                    PID_high                    |                  Signature.....               |\n
-- --------------------------------------------------------------------------------------------------\n
-- |                                        ....Signature....                                       |\n
-- --------------------------------------------------------------------------------------------------\n
-- |              ....Signature                     |                    Unused                     |\n
-- --------------------------------------------------------------------------------------------------\n
-- |                      TID                       |                     PID                       |\n
-- --------------------------------------------------------------------------------------------------\n
-- |                      UID                       |                     MID                       |\n
-- ------------------------------------------------------------------------------------------------- \n
--
-- All fields are, incidentally, encoded in little endian byte order. 
--
-- For the purposes here, the program doesn't care about most of the fields so they're given default 
-- values. The fields of interest are:\n
-- * Command -- The command of the packet (SMB_COM_NEGOTIATE, SMB_COM_SESSION_SETUP_ANDX, etc)\n
-- * UID/TID -- Sent by the server, and just have to be echoed back\n
--@param command The command to use.
--@return A string containing the packed packet header. 
function smb_get_header(command)

        -- Used for the header
        local smb = string.char(0xFF) .. "SMB"

        -- Pretty much every flags is deprecated. We set these two because they're required to be on. 
        local flags  = bit.bor(0x10, 0x08) -- SMB_FLAGS_CANONICAL_PATHNAMES | SMB_FLAGS_CASELESS_PATHNAMES
        -- These flags are less deprecated. We negotiate 32-bit status codes and long names. We also don't include 
Unicode, which tells
        -- the server that we deal in ASCII. 
        local flags2 = bit.bor(0x4000, 0x0040, 0x0001) -- SMB_FLAGS2_32BIT_STATUS | SMB_FLAGS2_IS_LONG_NAME | 
SMB_FLAGS2_KNOWS_LONG_NAMES

        local header = bin.pack("<CCCCCICSSLSSSSS",
                                smb:byte(1),  -- Header
                                smb:byte(2),  -- Header
                                smb:byte(3),  -- Header
                                smb:byte(4),  -- Header
                                command,      -- Command
                                0,            -- status
                                flags,        -- flags
                                flags2,       -- flags2
                                0,            -- extra (pid_high)
                                0,            -- extra (signature)
                                0,            -- extra (unused)
                                tid,          -- tid
                                0,            -- pid
                                uid,          -- uid
                                0             -- mid
                        )

        return header
end

--- Converts a string containing the parameters section into the encoded parameters string. 
-- The encoding is simple:\n
-- (1 byte)   The number of 2-byte values in the parameters section\n
-- (variable) The parameter section\n
-- @param parameters The parameters section. 
-- @return The encoded parameters. 
function smb_get_parameters(parameters)
        return bin.pack("<CA", string.len(parameters) / 2, parameters)
end

--- Converts a string containing the data section into the encoded data string. 
-- The encoding is simple:\n
-- (2 bytes)  The number of bytes in the data section\n
-- (variable) The data section\n
-- @param data The data section. 
-- @return The encoded data.
function smb_get_data(data)
        return bin.pack("<SA", string.len(data), data)
end

--- Prepends the NetBIOS header to the packet, which is essentially the length, encoded
--  in 4 bytes of big endian, and sends it out. The length is actually 17 or 24 bytes, 
--  depending on whether or not we're using raw, but that shouldn't matter. 
--@param socket The socket to send the packet on.
--@param header The header, encoded with smb_get_header().
--@param parameters The parameters, encoded with smb_get_parameters().
--@param data The data, encoded with, you guessed it, smb_get_data().
function smb_send(socket, header, parameters, data)
        local len = string.len(header) + string.len(parameters) + string.len(data)
        local out = bin.pack(">I<AAA", len, header, parameters, data)

        socket:send(out)
end

--- Reads the next packet from the socket, and parses it into the header, parameters, 
--  and data. 
-- [TODO] This assumes that exactly one packet arrives, which may not be the case. 
--        Some buffering should happen here. Currently, we're waiting on 32 bytes, which
--        is the length of the header, but there's no guarantee that we get the entire
--        body. 
--@param socket The socket to read the packet from
--@return (status, header, parameters, data) if status is true, the header, 
--        parameters, and data are all the raw arrays (with the lengths and such already
--        removed). If status is false, header contains an error message and parameters/
--        data are undefined. 
function smb_read(socket)
        local status, result
        local pos, length, header, parameter_length, parameters, data_length, data

        -- Receive the response
        -- [TODO] set the timeout length per jah's strategy:
        --   http://seclists.org/nmap-dev/2008/q3/0702.html
        socket:set_timeout(1000)
        status, result = socket:receive_bytes(32);

        -- Make sure the connection is still alive
        if(status ~= true) then
                return false, result
        end

        -- The length of the packet is 4 bytes of big endian (for our purposes).
        -- The header is 32 bytes.
        pos, length, header   = bin.unpack(">I<A32", result)
        -- The parameters length is a 1-byte value.
        pos, parameter_length = bin.unpack("<C",     result, pos)
        -- Double the length parameter, since parameters are two-byte values. 
        pos, parameters       = bin.unpack(string.format("<A%d", parameter_length*2), result, pos)
        -- The data length is a 2-byte value. 
        pos, data_length      = bin.unpack("<S",     result, pos)
        -- Read that many bytes of data.
        pos, data             = bin.unpack(string.format("<A%d", data_length),        result, pos)

        return status, header, parameters, data
end

--- Begins doing the SMB checks over a raw SMB connection (probably port 445). Since we 
--  don't need a header or session start or anything, we just make a conenction and pass 
--  it off to smb_start(). 
--@param host The host (or IP) to check.
--@param port The port to use (most likely 445).
--@return (status, result) if status is true, result is the string to display to the user. 
--        Otherwise, result is the error message. 
function smb_start_raw(host, port)
        local socket = nmap.new_socket()

        socket:connect(host, port, "tcp")

        return smb_start(socket)
end

--- Begins doing the SMB checks over a NetBIOS connection (probably port 139). To call this
--  function, the NetBIOS name has to be known. The netbios_get_server_name() function can
--  generally pull that value, if it isn't known. 
--@param host The host (or IP) to check.
--@param port The port to use (most likely 445).
--@param name The NetBIOS name of the server. 
--@return (status, result) if status is true, result is the string to display to the user. 
--        Otherwise, result is the error message. 
function smb_start_netbios(host, port, name)
        local pos, status, result, flags, length
        local socket = nmap.new_socket()

        -- Request a NetBIOS session
        session_request = bin.pack(">CCSzz", 
                                0x81,                        -- session request
                                0x00,                        -- flags
                                0x44,                        -- length
                                netbios_name_encode(name),   -- server name
                                netbios_name_encode("NMAP")  -- client name
                        );
        socket:connect(host, port, "tcp")
        socket:send(session_request)
        socket:set_timeout(1000)

        -- Receive the session response
        status, result = socket:receive_bytes(4);
        pos, result, flags, length = bin.unpack(">CCS", result)

        -- Check for a position session response (0x82)
        if result ~= 0x82 then
                return false, "Server refused to grant a NetBIOS session"
        end

        return smb_start(socket)
end

--- Calls the functions to send out the packets, and puts together most of the results. 
--  This is basically the core function in this script, and where most of the future 
--  additions will happen. 
--
-- Currently, it does the following:\n
-- * Sends out SMB_COM_NEGOTIATE_PROTOCOL\n
-- * Starts a NULL session\n
--   * Parses the OS\n
--   * Tries to connect to shares\n
-- * Tries to start a GUEST session\n
--   * Tries to connect to shares\n
-- * Tries to start an Administrator session (with a blank password)\n
--@param socket The socket to use for this connection (it is assumed that the function
--       can go ahead and start sending SMB traffic, so if the socket requires any kind
--       of startup, it has to be done already. 
function smb_start(socket)
        local status, result
        local response = ""
        local os = ""

        -- Negotiate protocol
        status, result = smb_negotiate_protocol(socket)
        if(status == false) then
                return false, result
        end
        response = response .. result

        -- Start a null session
        status, os = smb_start_session(socket, "")
        if(status == true) then
                response = response .. string.format("OS detection from SMB: %s\n", get_windows_version(os))

                -- See if it can connect to "IPC$"
                status = smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address))
                if(status == true) then
                        response = response .. "Null sessions enabled\n"
                else
                        response = response .. "Null sessions disabled\n"
                end
        
                -- Loop through a couple common shares, see if Null session has access
                for i,share in ipairs(shares) do
                        -- Try and connect
                        status, err = smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share))
                        if(status == true) then
                                response = response .. string.format("Null account has access to share '%s'\n", share)
                        elseif(err == 0xc0000022) then -- STATUS_ACCESS_DENIED
                                response = response .. string.format("Found a share '%s'\n", share)
                        end
                        -- Try with a '$' on the end
                        status = smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share))
                        if(status == true) then
                                response = response .. string.format("Null account has access to share '%s$'\n", share)
                        elseif(err == 0xc0000022) then -- STATUS_ACCESS_DENIED
                                response = response .. string.format("Found a share '%s$'\n", share)
                        end
                end
        end

        -- Start a guest session
        status, os = smb_start_session(socket, "GUEST")
        if(status == true) then

                -- See if it can connect to "IPC$"
                status = smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address))
                if(status == true) then
                        response = response .. "Guest account enabled\n"
                else
                        response = response .. "Guest account disabled\n"
                end

                -- Loop through a couple common shares, see if GUEST has access
                for i,share in ipairs(shares) do
                        -- Try and connect
                        status = smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share))
                        if(status == true) then
                                response = response .. string.format("Guest account has access to share '%s'\n", share)
                        end
                        -- Try with a '$' on the end
                        status = smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share))
                        if(status == true) then
                                response = response .. string.format("Guest account has access to share '%s$'\n", share)
                        end
                end
        else
                response = response .. "Guest account disabled\n"
        end

        -- Check if 'Administrator' has a blank password
        status, err = smb_start_session(socket, "ADMINISTRATOR")
        if(status == true) then
                response = response .. "Administrator account has a blank password\n"
        elseif(err == 0xc000006e) then -- STATUS_ACCOUNT_RESTRICTION
                response = response .. "Administrator account has a blank password (but can't use SMB)"
        
        end

        return true, response
end

--- Converts numbered Windows versions (5.0, 5.1) to the names (Windows 2000, Windows XP). 
--@param os The name of the OS
--@return The actual name of the OS (or the same as the 'os' parameter)
function get_windows_version(os)

        if(os == "Windows 5.0") then
                return "Windows 2000"
        elseif(os == "Windows 5.1")then
                return "Windows XP"
        end

        return os

end

--- Sends out SMB_COM_NEGOTIATE_PROTOCOL, which requests a session. 
-- Sends the following:\n
-- * List of known protocols\n
--\n
-- Receives:\n
-- * The prefered dialect\n
-- * The security mode\n
-- * Max number of multiplexed connectiosn, virtual circuits, and buffer sizes\n
-- * The server's system time and timezone\n
-- * The "encryption key" (aka, the server challenge)\n
-- * The capabilities\n
-- * The server and domain names\n
--@param socket The socket, in the proper state. 
--@return (status, result) If status is true, the result is a user-displayable string 
--        of the information cleaned. Otherwise, result is an error message. 
function smb_negotiate_protocol(socket)
        local response   = ""
        local header, parameters, data
        local pos
        local header1, header2, header3, ehader4, command, status, flags, flags2, pid_high, signature, unused, pid, mid
        local dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, time, timezone, key_length
        local date, timezone_str
        local domain, server

        header     = smb_get_header(0x72)

        -- Parameters are blank
        parameters = smb_get_parameters("")

        -- Data is a list of strings, terminated by a blank one. 
        data       = bin.pack("<CzCz", 2, "NT LM 0.12", 2, "")
        data       = smb_get_data(data)

        -- Send the negotiate request
        smb_send(socket, header, parameters, data)

        -- Read the result
        status, header, parameters, data = smb_read(socket)
        if(status ~= true) then
                return false, header
        end

        -- Since this is our first response, parse out the header
        pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, 
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)

        -- Parse the parameter section
        pos, dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, session_key, capabilities, time, 
timezone, key_length = bin.unpack("<SCSSIIIILsC", parameters)

        -- Check the security mode
        if(bit.band(security_mode, 1) == 1) then
                response = response .. "SMB Security: User-level authentication\n"
        else
                response = response .. "SMB Security: Share-level authentication\n"
        end

        -- Challenge/response supported?
        if(bit.band(security_mode, 2) == 0) then
                response = response .. "SMB Security: Plaintext only\n"
        else
                response = response .. "SMB Security: Challenge/response passwords supported\n"
        end

        -- Message signing supported/required?
        if(bit.band(security_mode, 8) == 8) then
                response = response .. "SMB Security: Message signing required\n"
        elseif(bit.band(security_mode, 4) == 4) then
                response = response .. "SMB Security: Message signing supported\n"
        else
                response = response .. "SMB Security: Message signing not supported\n"
        end

        -- Convert the time and timezone to more useful values
        time = (time / 10000000) - 11644473600
        date = os.date("%Y-%m-%d %H:%M:%S", time)
        timezone = -(timezone / 60)
        if(timezone == 0) then
                timezone_str = "UTC+0"
        elseif(timezone < 0) then
                timezone_str = "UTC-" .. math.abs(timezone)
        else
                timezone_str = "UTC+" .. timezone
        end
        response = response .. string.format("System time from SMB: %s [%s]\n", date, timezone_str)
        
        -- Data section
        -- This one's a little messier, because I don't appear to have unicode support
        pos, server_challenge = bin.unpack(string.format("<A%d", key_length), data)

        -- Get the domain as a Unicode string
        local ch, dummy
        domain = ""
        pos, ch, dummy = bin.unpack("<CC", data, pos)
        while ch ~= 0 do
                domain = domain .. string.char(ch)
                pos, ch, dummy = bin.unpack("<CC", data, pos)
        end

        -- Get the server name as a Unicode string
        server = ""
        pos, ch, dummy = bin.unpack("<CC", data, pos)
        while ch do
                server = server .. string.char(ch)
                pos, ch, dummy = bin.unpack("<CC", data, pos)
        end

        response = response .. string.format("Computer name from SMB: %s\\%s\n", domain, server)

        return true, response
end

--- Sends out SMB_COM_SESSION_START_ANDX, which attempts to log a user in. 
-- Sends the following:\n
-- * Negotiated parameters (multiplexed connections, virtual circuit, capabilities)\n
-- * Passwords (plaintext, unicode, lanman, ntlm, lmv2, ntlmv2, etc)\n
-- * Account name\n
-- * OS (I just send "Nmap")\n
-- * Native LAN Manager (no clue what that is, but it seems to be ignored)\n
--\n
-- Receives the following:\n
-- * User ID\n
-- * Server OS\n
--\n
--@param socket The socket, in the proper state. 
--@return (status, result) If status is true, the result is a user-displayable string 
--        of the information cleaned. Otherwise, result is an error message. 
function smb_start_session(socket, username)
        local status, result
        local response = ""
        local header, parameters, data
        local pos
        local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid 
        local andx_command, andx_reserved, andx_offset, action
        local os, lanmanager, domain

        header     = smb_get_header(0x73)

        -- Parameters
        parameters = bin.pack("<CCSSSSISSII", 
                                0xFF,        -- ANDX -- no further commands
                                0x00,        -- ANDX -- Reserved (0)
                                0x0000,      -- ANDX -- next offset
                                0x1000,      -- Max buffer size
                                0x0001,      -- Max multiplexes
                                0x0000,      -- Virtual circuit num
                                session_key, -- The session key
                                0,           -- ANSI/Lanman password length
                                0,           -- Unicode/NTLM password length
                                0,           -- Reserved
                capabilities -- Capabilities
                        )
        parameters = smb_get_parameters(parameters)

        -- Data is a list of strings, terminated by a blank one. 
        data       = bin.pack("<zzzz", 
                                                -- ANSI/Lanman password
                                                -- Unicode/NTLM password
                                username,       -- Account
                                "",             -- Domain
                                "Nmap",         -- OS
                                "Native Lanman" -- Native LAN Manager
                        )
        data       = smb_get_data(data)

        -- Send the session setup request
        smb_send(socket, header, parameters, data)

        -- Read the result
        status, header, parameters, data = smb_read(socket)
        if(status ~= true) then
                return false, header
        end

        -- Check if we were allowed in
        pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, 
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)
        if(status ~= 0) then
                return false, status
        end

        -- Parse the parameters
        pos, andx_command, andx_reserved, andx_offset, action = bin.unpack("<CCSS", parameters)
        
        -- Parse the data
        pos, os, lanmanager, domain = bin.unpack("<zzz", data)

        return true, os

end
 
--- Sends out SMB_COM_SESSION_TREE_CONNECT_ANDX, which attempts to connect to a share. 
-- Sends the following:\n
-- * Password (for share-level security, which we don't support)\n
-- * Share name\n
-- * Share type (or "?????" if it's unknown, that's what we do)\n
--\n
-- Receives the following:\n
-- * Tree ID\n
--\n
--@param socket The socket, in the proper state. 
--@return (status, result) If status is true, the result is a user-displayable string 
--        of the information cleaned. Otherwise, result is an error message. 
function smb_tree_connect(socket, path)
        local response = ""
        local header, parameters, data
        local pos
        local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid 
        local andx_command, andx_reserved, andx_offset, action

        header = smb_get_header(0x75)
        parameters = bin.pack("<CCSSS", 
                                        0xFF,   -- ANDX no further commands
                                        0x00,   -- ANDX reserved
                                        0x0000, -- ANDX offset
                                        0x0000, -- flags
                                        0x0000 -- password length (for share-level security)
                                )
        parameters = smb_get_parameters(parameters)

        data = bin.pack("zz", 
                                                -- Share-level password
                                        path,   -- Path
                                        "?????" -- Type of tree ("?????" = any)
                                )
        data = smb_get_data(data)

        -- Send the tree connect request
        smb_send(socket, header, parameters, data)

        -- Read the result
        status, header, parameters, data = smb_read(socket)
        if(status ~= true) then
                return false, header
        end

        -- Check if we were allowed in
        pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid, 
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)
        if(status ~= 0) then
                return false, status
        end

        return true
        
end

action = function(host)

        local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"})
        local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})

        address = host.ip
        

        if(port_t445 ~= nil and port_t445.state == "open") then
                -- Start the probes, raw
                port = 445
                status, result = smb_start_raw(host.ip, port)
        else
                -- Get the name
                status, name = netbios_get_server_name(host.ip)
                if(status == false) then
                        return "Error: " .. result
                end

                -- Start the probes, with the NetBIOS header
                port = 139
                status, result = smb_start_netbios(address, port, name)
        end

        return string.format("(using port %d):\n%s", port, result)
end



_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org

Current thread: