1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-07 13:11:28 +00:00
Files
nmap/nselib/netbios.lua
batrick 90a712ae2b Patch to libraries that were inappropriately using globals.
Often two (or more) scripts using the same library would
overwrite the globals each was using. This would result
in (at best) an error or (at worst) a deadlock.

The patch changes the global accesses to local.
2009-07-07 00:20:52 +00:00

403 lines
12 KiB
Lua

--- Creates and parses NetBIOS traffic. The primary use for this is to send
-- NetBIOS name requests.
--
--@author Ron Bowes <ron@skullsecurity.net>
--@copyright Same as Nmap--See http://nmap.org/book/man-legal.html
-----------------------------------------------------------------------
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:
-- * 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.
-- * L2: Prepend the length to the string, and to each substring in the scope
-- (separated by periods).
--@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
--@return 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 <code>do_nbstat</code>.
--@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 <code>do_nbstat</code>.
--@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:
--<code>
-- --------------------------------------------------
-- | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
-- | NAME_TRN_ID |
-- | R | OPCODE | NM_FLAGS | RCODE | (FLAGS)
-- | QDCOUNT |
-- | ANCOUNT |
-- | NSCOUNT |
-- | ARCOUNT |
-- --------------------------------------------------
--</code>
--
-- 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:
--
-- * (ntstring) requested name
-- * (2 bytes) query type
-- * (2 bytes) query class
-- * (2 bytes) time to live
-- * (2 bytes) record length
-- * (1 byte) number of names
-- * [for each name]
-- * (16 bytes) padded name, with a 1-byte suffix
-- * (2 bytes) flags
-- * (variable) statistics (usually mac address)
--
--@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(3, "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(3, " |_ [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)
local status, result = socket:receive_bytes(1)
if(status == false) then
return false, result
end
local close_status, err = socket:close()
if(close_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
if(rrlength > 0) then
rrlength = rrlength - 1
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