diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 07:42:04 +0000 |
commit | 0d47952611198ef6b1163f366dc03922d20b1475 (patch) | |
tree | 3d840a3b8c0daef0754707bfb9f5e873b6b1ac13 /nselib/netbios.lua | |
parent | Initial commit. (diff) | |
download | nmap-upstream.tar.xz nmap-upstream.zip |
Adding upstream version 7.94+git20230807.3be01efb1+dfsg.upstream/7.94+git20230807.3be01efb1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'nselib/netbios.lua')
-rw-r--r-- | nselib/netbios.lua | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/nselib/netbios.lua b/nselib/netbios.lua new file mode 100644 index 0000000..47b4d4d --- /dev/null +++ b/nselib/netbios.lua @@ -0,0 +1,489 @@ +--- +-- 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 https://nmap.org/book/man-legal.html + +local dns = require "dns" +local math = require "math" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +_ENV = stdnse.module("netbios", stdnse.seeall) + + +types = { + NB = 32, + NBSTAT = 33, +} + +--- 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.debug3("Encoding name '%s'", name) + -- Truncate or pad the string to 16 bytes + if(#name >= 16) then + name = string.sub(name, 1, 16) + else + local padding = " " + if name == "*" then + padding = "\0" + end + + name = name .. string.rep(padding, 16 - #name) + end + + -- Convert to uppercase + name = string.upper(name) + + -- Do the L1 encoding + local L1_encoded = {} + for i=1, #name, 1 do + local b = string.byte(name, i) + L1_encoded[i*2-1] = string.char(((b & 0xF0) >> 4) + 0x41) + L1_encoded[i*2] = string.char((b & 0x0F) + 0x41) + end + + -- Do the L2 encoding + local L2_encoded = { string.char(32), table.concat(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+1] = string.char(#piece) .. piece + end + end + + L2_encoded = table.concat(L2_encoded) + stdnse.debug3("=> '%s'", L2_encoded) + return L2_encoded +end + + + +--- 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.debug3("Decoding name '%s'", encoded_name) + + name = name:gsub("(.)(.)", function (a, b) + local ch = ((string.byte(a) - 0x41) << 4) | (string.byte(b) - 0x41) + return string.char(ch) + end) + + -- Decode the scope + local pos = 34 + while #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(#scope > 0) then + scope = string.sub(scope, 1, #scope - 1) + end + + stdnse.debug3("=> '%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 true, nil +end + +--- Sends out a UDP probe on port 137 to get the workstation's name (that is, the +-- unique entry in its NBSTAT table with a 0x00 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_workstation_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'] == 0x00 and (names[i]['flags'] & 0x8000) == 0 then + return true, names[i]['name'] + end + end + + return true, nil +end +--- Sends out a UDP probe on port 137 to get the user's name +-- +-- User name 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 + local reg + if type(host) == "string" then --ip + stdnse.debug3("Performing nbstat on host '%s'", host) + nmap.registry.netbios = nmap.registry.netbios or {} + nmap.registry.netbios[host] = nmap.registry.netbios[host] or {} + reg = nmap.registry.netbios[host] + else + stdnse.debug3("Performing nbstat on host '%s'", host.ip) + if host.registry.netbios == nil and + nmap.registry.netbios ~= nil and + nmap.registry.netbios[host.ip] ~= nil then + host.registry.netbios = nmap.registry.netbios[host.ip] + end + host.registry.netbios = host.registry.netbios or {} + reg = host.registry.netbios + end + + -- Check if it's cached in the registry for this host + if(reg["nbstat_names"] ~= nil) then + stdnse.debug3(" |_ [using cached value]") + return true, reg["nbstat_names"], reg["nbstat_statistics"] + end + + -- Create the query header + local query = string.pack(">I2I2I2I2I2I2", + 0x1337, -- Transaction id + 0x0000, -- Flags + 1, -- Questions + 0, -- Answers + 0, -- Authority + 0 -- Extra + ) .. string.pack(">zI2I2", + 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 + + TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, pos = string.unpack(">I2I2I2I2I2I2", 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 FLAGS & 0x8000 == 0 then + return false, "Server's flags didn't indicate a response" + end + if FLAGS & 0x0007 ~= 0 then + return false, string.format("Server returned a NetBIOS error: 0x%02x", FLAGS & 0x0007) + end + + -- Start parsing the answer field + rr_name, rr_type, rr_class, rr_ttl, pos = string.unpack(">zI2I2I4", 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 + + rrlength, name_count, pos = string.unpack(">I2B", 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. + name, suffix, flags, pos = string.unpack(">c15BI2", 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 + statistics, pos = string.unpack(string.format(">c%d", rrlength), result, pos) + + -- Put it in the registry, in case anybody else needs it + reg["nbstat_names"] = names + reg["nbstat_statistics"] = statistics + + return true, names, statistics + + else + return false, "Name query failed: " .. result + end +end + +function nbquery(host, nbname, options) + -- override any options or set the default values + local options = options or {} + options.port = options.port or 137 + options.retPkt = options.retPkt or true + options.dtype = options.dtype or types.NB + options.host = host.ip + options.flags = options.flags or ( options.multiple and 0x0110 ) + options.id = math.random(0xFFFF) + + -- encode and chop off the leading byte, as the dns library takes care of + -- specifying the length + local encoded_name = name_encode(nbname):sub(2) + + local status, response = dns.query( encoded_name, options ) + if ( not(status) ) then return false, "ERROR: nbquery failed" end + + local results = {} + -- discard any additional responses + if ( options.multiple and #response > 0 ) then + for _, resp in ipairs(response) do + assert( options.id == resp.output.id, "Received packet with invalid transaction ID" ) + if ( not(resp.output.answers) or #resp.output.answers < 1 ) then + return false, "ERROR: Response contained no answers" + end + local dname = string.char(#resp.output.answers[1].dname) .. resp.output.answers[1].dname + table.insert( results, { peer = resp.peer, name = name_decode(dname), data = resp.output.answers[1].data } ) + end + return true, results + else + local dname = string.char(#response.answers[1].dname) .. response.answers[1].dname + return true, { { peer = host.ip, name = name_decode(dname), data = response.answers[1].data } } + 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 flags & 0x8000 ~= 0 then + result[#result+1] = "<group>" + else + result[#result+1] = "<unique>" + end + + if flags & 0x1000 ~= 0 then + result[#result+1] = "<deregister>" + end + + if flags & 0x0800 ~= 0 then + result[#result+1] = "<conflict>" + end + + if flags & 0x0400 ~= 0 then + result[#result+1] = "<active>" + end + + if flags & 0x0200 ~= 0 then + result[#result+1] = "<permanent>" + end + + return table.concat(result) +end + + +return _ENV; |