Nmap Development mailing list archives

[NSE + NSELib] Netbios and SMB [stable!]


From: Ron <ron () skullsecurity net>
Date: Fri, 12 Sep 2008 14:33:13 -0500

Hey all,

I've spent the last week or two working on a NetBIOS and SMB library.
Finally, as of right now, I have a stable version that's documented,
clean, and works against every system I tried it on (with a minor
exception -- I'll talk about it below). So, with that, I'm submitting it
to be tested/beaten up as a real script.

I included two nselib libraries:
- smb.lua
- netbios.lua

And four scripts:
- nbstat.nse (enhanced version of the current one)
- smb-os-discovery.nse (makes netbios-smb-os-discovery.nse obsolete)
- smb-security-mode.nse
- smb-enum.nse

I am going to start working on new functionality over the next few
weeks, but as of right now it's useful and works, so it seems like a
good place to submit it.

The only time it DOESN'T work is against hosts with a specific firewall
configuration -- if ports 445 and 137 are blocked, but 139 is allowed
through, it will have trouble establishing a session. A couple tricks
are tried (using the generic '*SMBSERVER' name, and using the reverse
DNS name), but if those don't work it fails. I'm going to look for a way
to fix that, but I'm not sure if it's possible short of asking the user
for the server's name.

I also included
* Debug output if -d or -d -d are on (using stdnse library)
* Used Mutexes to help control connections (SMB doesn't like having
multiple connections from the same ip at the same time)
* nsedocs for every file/script/function
* Heavy use of 'bin' and 'bit' libraries for building packets

I should also say, thanks to those who've been working on those
libraries before me. Thanks to all the pre-written bits, this came
together incredibly fast and with little pain!

So yeah, comments are greatly appreciated!

Ron

PS: One of the next things I'd like to add is the ability to log into
SMB proper, with a user-submitted username/password. But first, I need
somebody to enhance OpenSSL bindings for Lua. Takers? :)
--- Sends a NetBIOS NBSTAT query to target host to try to determine the NetBIOS
--  names and MAC address. By default, displays the name of the computer and the
--  logged-in user; if verbosity is turned up, displays all names the system
--  thinks it owns. 
--
-- @output
-- (no verbose)
-- |_ NBSTAT: NetBIOS name: TEST1, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28
--
-- (verbose)
-- |  NBSTAT: NetBIOS name: TEST1, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28
-- |  Name: TEST1<00>            Flags: <unique><active>
-- |  Name: TEST1<20>            Flags: <unique><active>
-- |  Name: WORKGROUP<00>        Flags: <group><active>
-- |  Name: TEST1<03>            Flags: <unique><active>
-- |  Name: WORKGROUP<1e>        Flags: <group><active>
-- |  Name: RON<03>              Flags: <unique><active>
-- |  Name: WORKGROUP<1d>        Flags: <unique><active>
-- |_ Name: \x01\x02__MSBROWSE__\x02<01>  Flags: <group><active>

id = "NBSTAT"
description = "Sends a NetBIOS query to target host to try to determine \
the NetBIOS name and MAC address. For more information on the NetBIOS protocol, \
see 'nselib/netbios.lua'."
author = "Brandon Enright <bmenrigh () ucsd edu>, Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html";

-- Current version of this script was based entirly on Implementing CIFS, by 
-- Christopher R. Hertel. 
categories = {"default", "discovery", "safe"}

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
-- not explicit enough in this regard.  
hostrule = function(host)

        -- The following is an attempt to only run this script against hosts
        -- that will probably respond to a UDP 137 probe.  One might argue
        -- that sending a single UDP packet and waiting for a response is no
        -- big deal and that it should be done for every host.  In that case
        -- simply change this rule to always return true.

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

        if (
                (port_t135 ~= nil and port_t135.state == "open") or
                (port_t139 ~= nil and port_t139.state == "open") or
                (port_t445 ~= nil and port_t445.state == "open") or
                (port_u137 ~= nil and
                        (port_u137.state == "open" or
                        port_u137.state == "open|filtered")))
        then
                return true
        else
                return false
        end     
end


action = function(host)

        local i
        local status
        local names, statistics
        local server_name, user_name
        local mac
        local result = ""

        -- 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

        -- Get the server name
        status, server_name = netbios.get_server_name(host.ip, names)
        if(status == false) then
                return "ERROR: " .. server_name
        end

        -- Get the logged in user
        status, user_name = netbios.get_user_name(host.ip, names)
        if(status == false) then
                return "ERROR: " .. user_name
        end

        -- 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 = "<unknown>"
        end

        -- Check if we actually got a username
        if(user_name == nil) then
                user_name = "<unknown>"
        end

        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 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


        return result

end
--- 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


--- 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


--- 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


--- Creates and parses NetBIOS traffic. The primary use for this is to send
--  NetBIOS name requests. 
--
--@author Ron Bowes <ron () skullsecurity net>
--@copyright See nmaps COPYING for licence
-----------------------------------------------------------------------

module(... or "netbios", package.seeall)

require 'bit'
require 'bin'
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 .. "<group>"
        else
                result = result .. "<unique>"
        end

        if(bit.band(flags, 0x1000) ~= 0) then
                result = result .. "<deregister>"
        end

        if(bit.band(flags, 0x0800) ~= 0) then
                result = result .. "<conflict>"
        end

        if(bit.band(flags, 0x0400) ~= 0) then
                result = result .. "<active>"
        end

        if(bit.band(flags, 0x0200) ~= 0) then
                result = result .. "<permanent>"
        end

        return result
end

--- A library for SMB (Server Message Block) (aka CIFS) traffic. This traffic is normally
--  sent to/from ports 139 or 445 of Windows systems, although it's also implemented by
--  others (the most notable one being Samba). 
--
-- The intention of this library is toe ventually handle all aspects of the SMB protocol,
-- A programmer using this library must already have some knowledge of the SMB protocol, 
-- although a lot isn't necessary. You can pick up a lot by looking at the code that uses
-- this. The basic login is this:
--
-- [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 <ron () skullsecurity net>
--@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("<CCCCCICSSLSSSSS",
                                smb:byte(1),  -- Header
                                smb:byte(2),  -- Header
                                smb:byte(3),  -- Header
                                smb:byte(4),  -- Header
                                command,      -- Command
                                0,            -- status
                                flags,        -- flags
                                flags2,       -- flags2
                                0,            -- extra (pid_high)
                                0,            -- extra (signature)
                                0,            -- extra (unused)
                                tid,          -- tid
                                0,            -- pid
                                uid,          -- uid
                                0             -- mid
                        )

        return header
end

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

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

--- Prepends the NetBIOS header to the packet, which is essentially the length, encoded
--  in 4 bytes of big endian, and sends it out. The length field is actually 17 or 24 bits 
--  wide, depending on whether or not we're using raw, but that shouldn't matter. 
--
--@param socket The socket to send the packet on.
--@param header The header, encoded with smb_get_header().
--@param parameters The parameters
--@param data The data
--@return (result, err) If result is false, err is the error message. Otherwise, err is
--        undefined
function smb_send(socket, header, parameters, data)
    local encoded_parameters = smb_encode_parameters(parameters)
    local encoded_data       = smb_encode_data(data)
    local len = string.len(header) + string.len(encoded_parameters) + string.len(encoded_data)
    local out = bin.pack(">I<AAA", len, header, encoded_parameters, encoded_data)

        stdnse.print_debug(2, "Sending SMB packet (len: %d)", string.len(out))
    return socket:send(out)
end

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

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

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

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

        stdnse.print_debug(2, "Received %d bytes from SMB", string.len(result))
        return status, header, parameters, data
end

--- Sends out SMB_COM_NEGOTIATE_PROTOCOL, which is typically the first SMB packet sent out. 
-- Sends the following:\n
-- * List of known protocols\n
--\n
-- Receives:\n
-- * The prefered dialect\n
-- * The security mode\n
-- * Max number of multiplexed connectiosn, virtual circuits, and buffer sizes\n
-- * The server's system time and timezone\n
-- * The "encryption key" (aka, the server challenge)\n
-- * The capabilities\n
-- * The server and domain names\n
--@param socket The socket, in the proper state (ie, newly connected). 
--@return (status, result) If status is false, result is an error message. Otherwise, result is a 
--        table with the following elements:\n
--      'security_mode'    Whether or not to use cleartext passwords, message signatures, etc.\n
--      'max_mpx'          Maximum number of multiplexed connections\n
--      'max_vc'           Maximum number of virtual circuits\n
--      'max_buffer'       Maximum buffer size\n
--      'max_raw_buffer'   Maximum buffer size for raw connections (considered obsolete)\n
--      'session_key'      A value that's basically just echoed back\n
--      'capabilities'     The server's capabilities\n
--      'time'             The server's time (in UNIX-style seconds since 1970)\n
--      'date'             The server's date in a user-readable format\n
--      'timezone'         The server's timezone, in hours from UTC\n
--      'timezone_str'     The server's timezone, as a string\n
--      'server_challenge' A random string used for challenge/response\n
--      'domain'           The server's primary domain\n
--      'server'           The server's name\n
function negotiate_protocol(socket)
        local header, parameters, data
        local pos
        local header1, header2, header3, ehader4, command, status, flags, flags2, pid_high, signature, unused, pid, mid
        local dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, session_key, capabilities, time, 
timezone, key_length
        local server_challenge, date, timezone_str
        local domain, server
        local response = {}

        header     = smb_encode_header(0x72, 0, 0)

        -- Parameters are blank
        parameters = ""

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

        -- Send the negotiate request
        stdnse.print_debug(2, "Sending SMB_COM_NEGOTIATE_PROTOCOL")
        result, err = smb_send(socket, header, parameters, data)
        if(status == false) then
                return err
        end

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

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

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

        -- Convert the time and timezone to more useful values
        time = (time / 10000000) - 11644473600
        date = os.date("%Y-%m-%d %H:%M:%S", time)
        timezone = -(timezone / 60)
        if(timezone == 0) then
                timezone_str = "UTC+0"
        elseif(timezone < 0) then
                timezone_str = "UTC-" .. math.abs(timezone)
        else
                timezone_str = "UTC+" .. timezone
        end

        -- Data section
        -- This one's a little messier, because I don't appear to have unicode support
        pos, server_challenge = bin.unpack(string.format("<A%d", key_length), data)

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

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

        -- Fill out response variables
        response['security_mode']    = security_mode
        response['max_mpx']          = max_mpx
        response['max_vc']           = max_vc
        response['max_buffer']       = max_buffer
        response['max_raw_buffer']   = max_raw_buffer
        response['session_key']      = session_key
        response['capabilities']     = capabilities
        response['time']             = time
        response['date']             = date
        response['timezone']         = timezone
        response['timezone_str']     = timezone_str
        response['server_challenge'] = server_challenge
        response['domain']           = domain
        response['server']           = server

        return true, response
end

--- Sends out SMB_COM_SESSION_START_ANDX, which attempts to log a user in. 
-- Sends the following:\n
-- * Negotiated parameters (multiplexed connections, virtual circuit, capabilities)\n
-- * Passwords (plaintext, unicode, lanman, ntlm, lmv2, ntlmv2, etc)\n
-- * Account name\n
-- * OS (I just send "Nmap")\n
-- * Native LAN Manager (no clue what that is, but it seems to be ignored)\n
--\n
-- Receives the following:\n
-- * User ID\n
-- * Server OS\n
--\n
--@param socket       The socket, in the proper state (ie, after protocol has been negotiated).
--@param username     The account name to use. For Null sessions, leave it blank (''). 
--@param session_key  The session_key value, returned by SMB_COM_NEGOTIATE_PROTOCOL.  
--@param capabilities The server's capabilities, returned by SMB_COM_NEGOTIATE_PROTOCOL. 
--@return (status, result) If status is false, result is an error message. Otherwise, result is a 
--        table with the following elements:\n
--      'uid'         The UserID for the session
--      'is_guest'    If set, the username wasn't found so the user was automatically logged in
--                    as the guest account
--      'os'          The operating system
--      'lanmanager'  The servers's LAN Manager
function start_session(socket, username, session_key, capabilities)
        local status, result
        local header, parameters, data
        local pos
        local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, 
pid, uid, mid 
        local andx_command, andx_reserved, andx_offset, action
        local os, lanmanager, domain
        local response = {}

        header     = smb_encode_header(0x73, 0, 0)

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

        -- Data is a list of strings, terminated by a blank one. 
        data       = bin.pack("<zzzz", 
                                                -- ANSI/Lanman password
                                                -- Unicode/NTLM password
                                username,       -- Account
                                "",             -- Domain
                                "Nmap",         -- OS
                                "Native Lanman" -- Native LAN Manager
                        )
        -- Send the session setup request
        stdnse.print_debug(2, "Sending SMB_COM_SESSION_SETUP_ANDX")
        result, err = smb_send(socket, header, parameters, data)
        if(result == false) then
                return false, err
        end

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

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

        -- Parse the parameters
        pos, andx_command, andx_reserved, andx_offset, action = bin.unpack("<CCSS", parameters)

        -- Parse the data
        pos, os, lanmanager, domain = bin.unpack("<zzz", data)

        -- Fill in the response string
        response['uid']        = uid
        response['is_guest']   = bit.band(action, 1)
        response['os']         = os
        response['lanmanager'] = lanmanager

        return true, response

end
 
--- Sends out SMB_COM_SESSION_TREE_CONNECT_ANDX, which attempts to connect to a share. 
-- Sends the following:\n
-- * Password (for share-level security, which we don't support)\n
-- * Share name\n
-- * Share type (or "?????" if it's unknown, that's what we do)\n
--\n
-- Receives the following:\n
-- * Tree ID\n
--\n
--@param socket The socket, in the proper state. 
--@param path   The path to connect (eg, \\servername\C$)
--@param uid    The UserID, returned by SMB_COM_SESSION_SETUP_ANDX
--@return (status, result) If status is false, result is an error message. Otherwise, result is a 
--        table with the following elements:\n
--      'tid'         The TreeID for the session
function tree_connect(socket, path, uid)
        local response = ""
        local header, parameters, data
        local pos
        local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid 
        local andx_command, andx_reserved, andx_offset, action
        local response = {}

        header = smb_encode_header(0x75, uid, 0)
        parameters = bin.pack("<CCSSS", 
                                        0xFF,   -- ANDX no further commands
                                        0x00,   -- ANDX reserved
                                        0x0000, -- ANDX offset
                                        0x0000, -- flags
                                        0x0000 -- password length (for share-level security)
                                )
        data = bin.pack("zz", 
                                                -- Share-level password
                                        path,   -- Path
                                        "?????" -- Type of tree ("?????" = any)
                                )

        -- Send the tree connect request
        stdnse.print_debug(2, "Sending SMB_COM_TREE_CONNECT_ANDX")
        result, err = smb_send(socket, header, parameters, data)
        if(result == false) then
                return false, err
        end

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

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

        response['tid'] = tid

        return true, response
        
end


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

Current thread: