mirror of
https://github.com/nmap/nmap.git
synced 2025-12-13 11:19:02 +00:00
Add Ron Bowes's netbios and smb NSE modules and new scripts that use them. They
were introduced in http://seclists.org/nmap-dev/2008/q3/0827.html.
This commit is contained in:
12
CHANGELOG
12
CHANGELOG
@@ -1,5 +1,17 @@
|
|||||||
# Nmap Changelog ($Id$); -*-text-*-
|
# Nmap Changelog ($Id$); -*-text-*-
|
||||||
|
|
||||||
|
o Added two new nselib modules, netbios and smb, that contain common
|
||||||
|
code for scripts using NetBIOS and SMB. Also added or updated four
|
||||||
|
scripts that use the new modules:
|
||||||
|
nbstat.nse: get NetBIOS names and MAC address.
|
||||||
|
smb-enum.nse: enumerate SMB users and shares.
|
||||||
|
smb-os-discovery.nse: get operating system over SMB (replaces
|
||||||
|
netbios-smb-os-discovery.nse).
|
||||||
|
smb-security-mode.nse: determine if a host uses user-level or
|
||||||
|
share-level security, and what other security features it
|
||||||
|
supports.
|
||||||
|
[Ron Bowes]
|
||||||
|
|
||||||
o A script could be executed twice if it was given with the --script
|
o A script could be executed twice if it was given with the --script
|
||||||
option, also in the "version" category, and version detection (-sV)
|
option, also in the "version" category, and version detection (-sV)
|
||||||
was requested. This has been fixed. [David]
|
was requested. This has been fixed. [David]
|
||||||
|
|||||||
396
nselib/netbios.lua
Normal file
396
nselib/netbios.lua
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
--- 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
|
||||||
|
|
||||||
730
nselib/smb.lua
Normal file
730
nselib/smb.lua
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
--- 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
|
||||||
|
|
||||||
@@ -1,17 +1,35 @@
|
|||||||
|
--- 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"
|
id = "NBSTAT"
|
||||||
description = "Sends a NetBIOS query to target host to try to determine \
|
description = "Sends a NetBIOS query to target host to try to determine \
|
||||||
the NetBIOS name and MAC address."
|
the NetBIOS name and MAC address. For more information on the NetBIOS protocol, \
|
||||||
author = "Brandon Enright <bmenrigh@ucsd.edu>"
|
see 'nselib/netbios.lua'."
|
||||||
|
author = "Brandon Enright <bmenrigh@ucsd.edu>, Ron Bowes"
|
||||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||||
|
|
||||||
-- This script was created by reverse-engineering the packets
|
-- Current version of this script was based entirly on Implementing CIFS, by
|
||||||
-- sent by NBTSCAN and hacking with the Wireshark NetBIOS
|
-- Christopher R. Hertel.
|
||||||
-- protocol dissector. I do not believe this constitutes
|
|
||||||
-- a derivative work in the GPL sense of the phrase.
|
|
||||||
|
|
||||||
categories = {"default", "discovery", "safe"}
|
categories = {"default", "discovery", "safe"}
|
||||||
|
|
||||||
require "comm"
|
require "netbios"
|
||||||
|
|
||||||
-- I have excluded the port function param because it doesn't make much sense
|
-- 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
|
-- for a hostrule. It works without warning. The NSE documentation is
|
||||||
@@ -48,139 +66,69 @@ hostrule = function(host)
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Again, I have excluded the port param. Is this okay on a hostrule?
|
|
||||||
action = function(host)
|
action = function(host)
|
||||||
|
|
||||||
-- This is the UDP NetBIOS request packet. I didn't feel like
|
|
||||||
-- actually generating a new one each time so this has been shamelessly
|
|
||||||
-- copied from a packet dump of nbtscan.
|
|
||||||
-- See http://www.unixwiz.net/tools/nbtscan.html for code.
|
|
||||||
-- The magic number in this code is \003\097.
|
|
||||||
local data =
|
|
||||||
"\003\097\000\016\000\001\000\000" ..
|
|
||||||
"\000\000\000\000\032\067\075\065" ..
|
|
||||||
"\065\065\065\065\065\065\065\065" ..
|
|
||||||
"\065\065\065\065\065\065\065\065" ..
|
|
||||||
"\065\065\065\065\065\065\065\065" ..
|
|
||||||
"\065\065\065\065\065\000\000\033" ..
|
|
||||||
"\000\001"
|
|
||||||
|
|
||||||
local status, result = comm.exchange(host, 137, data, {proto="udp", timeout=5000})
|
local i
|
||||||
|
local status
|
||||||
|
local names, statistics
|
||||||
|
local server_name, user_name
|
||||||
|
local mac
|
||||||
|
local result = ""
|
||||||
|
|
||||||
if (not status) then
|
-- Get the list of NetBIOS names
|
||||||
return
|
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
|
end
|
||||||
|
|
||||||
-- We got data back from 137, make sure we know it is open
|
-- Get the server name
|
||||||
nmap.set_port_state(host, {number=137, protocol="udp"}, "open")
|
status, server_name = netbios.get_server_name(host.ip, names)
|
||||||
|
if(status == false) then
|
||||||
-- Magic numbers:
|
return "ERROR: " .. server_name
|
||||||
-- Offset to number of names returned: 57
|
|
||||||
-- Useful name length: 15
|
|
||||||
-- Name type length: 3
|
|
||||||
-- Computer name type: \032\068\000 or \032\004\000
|
|
||||||
-- User name type: \003\068\000 or \003\004\000
|
|
||||||
-- Length of each name + name type: 19
|
|
||||||
-- Length of MAC address: 6
|
|
||||||
-- Note that string.sub includes a 0 char so these numbers are 1 less
|
|
||||||
|
|
||||||
if (string.len(result) < 57) then
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Make sure the response at least looks like a NBTSTAT response
|
-- Get the logged in user
|
||||||
-- The first 2 bytes are the magic number sent originally, The second
|
status, user_name = netbios.get_user_name(host.ip, names)
|
||||||
-- 2 bytes should be 0x84 0x00 (errorless name query response)
|
if(status == false) then
|
||||||
if (string.sub(result, 1, 4) ~= "\003\097\132\000" ) then
|
return "ERROR: " .. user_name
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local namenum = string.byte(result, 57)
|
-- 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))
|
||||||
if (string.len(result) < 58 + namenum * 18 + 6) then
|
-- Samba doesn't set the Mac address
|
||||||
return
|
if(mac == "00:00:00:00:00:00") then
|
||||||
|
mac = "<unknown>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check if we actually got a username
|
||||||
|
if(user_name == nil) then
|
||||||
|
user_name = "<unknown>"
|
||||||
|
end
|
||||||
|
|
||||||
-- This loop will try to find the computer name. This name needs to
|
result = result .. string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s\n", server_name, user_name, mac)
|
||||||
-- be found before the username because sometimes NetBIOS reports
|
|
||||||
-- username flags with the computer name as text.
|
-- If verbosity is set, dump the whole list of names
|
||||||
local compname
|
if(nmap.verbosity() >= 1) then
|
||||||
for i = 0, namenum - 1, 1 do
|
for i = 1, #names, 1 do
|
||||||
-- Names come back trailing-space-padded so strip that off..
|
local padding = string.rep(" ", 17 - string.len(names[i]['name']))
|
||||||
local namefield = string.sub (result, 58 + i * 18,
|
local flags_str = netbios.flags_to_string(names[i]['flags'])
|
||||||
58 + i * 18 + 14)
|
result = result .. string.format("Name: %s<%02x>%sFlags: %s\n", names[i]['name'], names[i]['suffix'], padding, flags_str)
|
||||||
local iname
|
|
||||||
local nameflags = string.sub (result, 58 + i * 18 + 15,
|
|
||||||
58 + i * 18 + 15 + 2)
|
|
||||||
local padindex = string.find(namefield, " ")
|
|
||||||
if (padindex ~= nil and padindex > 1) then
|
|
||||||
iname = string.sub(namefield, 1, padindex - 1)
|
|
||||||
else
|
|
||||||
iname = namefield
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if (nameflags == "\032\068\000" or
|
-- If super verbosity is set, print out the full statistics
|
||||||
nameflags == "\032\004\000") then
|
if(nmap.verbosity() >= 2) then
|
||||||
compname = iname
|
result = result .. "Statistics: "
|
||||||
end
|
for i = 1, #statistics, 1 do
|
||||||
end
|
result = result .. string.format("%02x ", statistics:byte(i))
|
||||||
|
|
||||||
if (compname == nil) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- This loop will attempt to find the username logged onto the machine
|
|
||||||
-- This is not possible on most Windows machines (I don't know why)
|
|
||||||
-- Sometimes the flag that generally indicates the username
|
|
||||||
-- returns the computer name instead. This function will ignore
|
|
||||||
-- the username if it matches the computer name. This loop will not
|
|
||||||
-- properly report the the username if it really happens to be
|
|
||||||
-- the same as the computer name.
|
|
||||||
local username
|
|
||||||
for i = 0, namenum - 1, 1 do
|
|
||||||
-- Names come back trailing-space-padded so strip that off..
|
|
||||||
local namefield = string.sub (result, 58 + i * 18,
|
|
||||||
58 + i * 18 + 14)
|
|
||||||
local iname
|
|
||||||
local nameflags = string.sub (result, 58 + i * 18 + 15,
|
|
||||||
58 + i * 18 + 15 + 2)
|
|
||||||
local padindex = string.find(namefield, " ")
|
|
||||||
if (padindex ~= nil and padindex > 1) then
|
|
||||||
iname = string.sub(namefield, 1, padindex - 1)
|
|
||||||
else
|
|
||||||
iname = namefield
|
|
||||||
end
|
|
||||||
|
|
||||||
if (nameflags == "\003\068\000" or
|
|
||||||
nameflags == "\003\004\000") then
|
|
||||||
if (string.find(iname, compname, 1, true) == nil) then
|
|
||||||
username = iname
|
|
||||||
end
|
end
|
||||||
|
result = result .. "\n"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- SAMBA likes to say its MAC is all 0s. That could be detected...
|
return result
|
||||||
-- If people say printing a MAC of 0000.0000.000 is more wrong
|
|
||||||
-- than not returning a MAC at all then fix it here.
|
|
||||||
local macfield = string.sub (result, 58 + namenum * 18,
|
|
||||||
58 + namenum * 18 + 5)
|
|
||||||
local mac = string.format ("%02X:%02X:%02X:%02X:%02X:%02X",
|
|
||||||
string.byte(macfield, 1),
|
|
||||||
string.byte(macfield, 2),
|
|
||||||
string.byte(macfield, 3),
|
|
||||||
string.byte(macfield, 4),
|
|
||||||
string.byte(macfield, 5),
|
|
||||||
string.byte(macfield, 6))
|
|
||||||
|
|
||||||
if (username ~= nil) then
|
|
||||||
return "NetBIOS name: " .. compname ..
|
|
||||||
", NetBIOS user: " .. username ..
|
|
||||||
", NetBIOS MAC: " .. mac
|
|
||||||
else
|
|
||||||
return "NetBIOS name: " .. compname ..
|
|
||||||
", NetBIOS MAC: " .. mac
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,456 +0,0 @@
|
|||||||
--- This script probes a target for its operating system version.
|
|
||||||
-- It sends traffic via UDP port 137 and TCP port 139/445.\n\n
|
|
||||||
-- == Implementation Information ==\n
|
|
||||||
-- First, we need to
|
|
||||||
-- elicit the NetBIOS share name associated with a workstation share.
|
|
||||||
-- Once we have that, we need to encode the name into the "mangled"
|
|
||||||
-- equivalent and send TCP 139/445 traffic to connect to the host and
|
|
||||||
-- in an attempt to elicit the OS version name from an SMB Setup AndX
|
|
||||||
-- response.\n\n
|
|
||||||
--
|
|
||||||
-- Thanks to Michail Prokopyev and xSharez Scanner for required
|
|
||||||
-- traffic to generate for OS version detection.
|
|
||||||
--
|
|
||||||
--@usage
|
|
||||||
-- sudo nmap -sU -sS --script netbios-smb-os-discovery.nse -p U:137,T:139 127.0.0.1
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
|
|
||||||
id = "Discover OS Version over NetBIOS and SMB"
|
|
||||||
description = "Attempt to elicit OS version from host running NetBIOS/SMB"
|
|
||||||
author = "Judy Novak"
|
|
||||||
copyright = "Sourcefire Inc, (C) 2006-2007"
|
|
||||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
|
||||||
categories = {"version"}
|
|
||||||
|
|
||||||
require 'bit'
|
|
||||||
|
|
||||||
hostrule = function(host)
|
|
||||||
|
|
||||||
-- This script should run under two different conditions:
|
|
||||||
-- a) port tcp/445 is open (allowing us to make a raw connection)
|
|
||||||
-- b) ports tcp/139 and udp/137 are open (137 may not be known)
|
|
||||||
|
|
||||||
local port_u137 = nmap.get_port_state(host, {number=137, protocol="udp"})
|
|
||||||
local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"})
|
|
||||||
local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})
|
|
||||||
|
|
||||||
if(port_t445 ~= nil and port_t445.state == "open") then
|
|
||||||
-- tcp/445 is open, we're good
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
if(port_t139 ~= nil and port_t139.state == "open") then
|
|
||||||
-- tcp/139 is open, check uf udp/137 is open or unknown
|
|
||||||
if(port_u137 == nil or port_u137.state == "open" or port_u137.state == "open|filtered") then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
action = function(host)
|
|
||||||
local sharename, message, osversion, currenttime, gen_msg, gen_msg_time, x
|
|
||||||
|
|
||||||
sharename = 0
|
|
||||||
osversion = ""
|
|
||||||
gen_msg = "OS version cannot be determined.\n"
|
|
||||||
gen_msg_time = "System time cannot be determined.\n"
|
|
||||||
|
|
||||||
-- Decide whether to use raw SMB (port 445) or SMB over NetBIOS (139).
|
|
||||||
-- Raw is better, because it uses one less packet and doesn't require a
|
|
||||||
-- name to be known.
|
|
||||||
local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})
|
|
||||||
|
|
||||||
local use_raw = (port_t445 ~= nil and port_t445.state == "open")
|
|
||||||
|
|
||||||
if(not use_raw) then
|
|
||||||
sharename, message = udp_query(host)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ret = ""
|
|
||||||
|
|
||||||
if (use_raw or sharename ~= 0) then
|
|
||||||
osversion, currenttime, message = tcp_session(sharename, host, use_raw)
|
|
||||||
if (osversion ~= 0) then
|
|
||||||
ret = ret .. osversion
|
|
||||||
if(currenttime ~= 0) then
|
|
||||||
ret = ret .. "\n" .. "Discover system time over SMB: " .. currenttime
|
|
||||||
else
|
|
||||||
ret = ret .. "\n" .. gen_msg_time .. message
|
|
||||||
end
|
|
||||||
else
|
|
||||||
ret = ret .. gen_msg .. message
|
|
||||||
end
|
|
||||||
else
|
|
||||||
ret = ret .. gen_msg .. "TCP/445 closed and couldn't determine NetBIOS name"
|
|
||||||
end
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- A NetBIOS wildcard query is sent to a host in an attempt to discover
|
|
||||||
-- any NetBIOS shares on the host.
|
|
||||||
|
|
||||||
function udp_query(host)
|
|
||||||
|
|
||||||
local l, sharename, message
|
|
||||||
local WildCard =
|
|
||||||
string.char(0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x20, 0x43, 0x4b, 0x41, 0x41, 0x41, 0x41, 0x41,
|
|
||||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
|
||||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
|
||||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x00)
|
|
||||||
|
|
||||||
local socket = nmap.new_socket()
|
|
||||||
|
|
||||||
socket:connect(host.ip, 137, "udp")
|
|
||||||
socket:send(WildCard)
|
|
||||||
socket:set_timeout(100)
|
|
||||||
|
|
||||||
local status, result = socket:receive_bytes(1);
|
|
||||||
|
|
||||||
socket:close()
|
|
||||||
|
|
||||||
if (result ~= nil) then
|
|
||||||
l = string.len(result)
|
|
||||||
sharename = extract_sharename(result)
|
|
||||||
if (sharename ~= 0) then
|
|
||||||
return sharename, 1
|
|
||||||
else
|
|
||||||
message = "Failed to find NetBIOS share name in response to UDP NetBIOS wildcard query"
|
|
||||||
return 0, message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- This function extracts the name of a "workstation" share from the
|
|
||||||
-- response to the UDP NetBIOS wildcard query. Typically, there are
|
|
||||||
-- several share types returned, but only one with a "workstation"
|
|
||||||
-- type/code can be queried later for the OS version. The workstation
|
|
||||||
-- type/code is 0x44 0x00 for OS versions prior to Vista. The type/code
|
|
||||||
-- for Vista is 0x04 0x00.
|
|
||||||
|
|
||||||
function extract_sharename(resp)
|
|
||||||
|
|
||||||
local lenpay, beg, eend, typebeg, typeend, temp, name, nametype, ntgeneric, ntvista, ename, myname, eename, ntunix
|
|
||||||
|
|
||||||
beg = 58
|
|
||||||
eend = beg + 15
|
|
||||||
typebeg = eend + 1
|
|
||||||
lenpay = string.len(resp)
|
|
||||||
|
|
||||||
while (eend <= lenpay) do
|
|
||||||
|
|
||||||
myname = string_concatenate(resp, beg, eend - 1)
|
|
||||||
nametype = string.byte(resp, typebeg) .. string.byte(resp, typebeg + 1)
|
|
||||||
ntgeneric = string.find(nametype, 0x44,0x00)
|
|
||||||
ntvista = string.find(nametype, 0x04, 0x00)
|
|
||||||
ntunix = string.find(nametype, 0x64, 0x00)
|
|
||||||
|
|
||||||
if (ntgeneric == 1) or (ntvista == 1) or (ntunix == 1) then
|
|
||||||
ename = encode(myname)
|
|
||||||
end
|
|
||||||
|
|
||||||
if (ename ~= nil) then
|
|
||||||
do
|
|
||||||
ename = string.char(0x20) .. ename .. string.char(0x43, 0x41, 0x00)
|
|
||||||
return(ename)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
beg = beg + 18
|
|
||||||
eend = beg + 15
|
|
||||||
typebeg = eend + 1
|
|
||||||
end
|
|
||||||
return(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- Extract multiple bytes from a string and return concatenated result
|
|
||||||
|
|
||||||
function string_concatenate(mystring, start, stop)
|
|
||||||
local x, temp, newname
|
|
||||||
|
|
||||||
for x = start, stop, 1 do
|
|
||||||
temp = string.byte(mystring,x)
|
|
||||||
if (x > start) then
|
|
||||||
newname = newname .. string.char(temp)
|
|
||||||
else
|
|
||||||
newname = string.char(temp)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return(newname)
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- This function encodes the workstation share name returned from the
|
|
||||||
-- UDP wildcard NetBIOS query. Each character from the NetBIOS share
|
|
||||||
-- name is encoded/mangled using a special algorithm. Rather than
|
|
||||||
-- implementing the algorithm, Microsoft offers a conversion table for
|
|
||||||
-- any valid character found in a share name. I could not figure out
|
|
||||||
-- how to use a Lua dictionary where the key value included a
|
|
||||||
-- non-alphanumeric character. The static variable chars represents
|
|
||||||
-- most of the characters that can be found in a share and the position
|
|
||||||
-- in the string "chars" is the corresponding position in the trtable
|
|
||||||
-- table. The character " had to be handled separately as it is used
|
|
||||||
-- to delimit the value of chars.
|
|
||||||
|
|
||||||
encode = function(name)
|
|
||||||
|
|
||||||
local ln, y, nchar, newname, pos, temp, trtable
|
|
||||||
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !#$%&'()*+,-.=:;@^_{}~"
|
|
||||||
|
|
||||||
local trtable =
|
|
||||||
{
|
|
||||||
string.char(0x45,0x42), string.char(0x45,0x43), string.char(0x45,0x44), string.char(0x45,0x45), string.char(0x45,0x46),
|
|
||||||
string.char(0x45,0x47), string.char(0x45,0x48), string.char(0x45,0x49), string.char(0x45,0x4A), string.char(0x45,0x4B),
|
|
||||||
string.char(0x45,0x4C), string.char(0x45,0x4D), string.char(0x45,0x4E), string.char(0x45,0x4F), string.char(0x45,0x50),
|
|
||||||
string.char(0x46,0x41), string.char(0x46,0x42), string.char(0x46,0x43), string.char(0x46,0x44), string.char(0x46,0x45),
|
|
||||||
string.char(0x46,0x46), string.char(0x46,0x47), string.char(0x46,0x48), string.char(0x46,0x49), string.char(0x46,0x4A),
|
|
||||||
string.char(0x46,0x4B), string.char(0x44,0x41), string.char(0x44,0x42), string.char(0x44,0x43), string.char(0x44,0x44),
|
|
||||||
string.char(0x44,0x45), string.char(0x44,0x46), string.char(0x44,0x47), string.char(0x44,0x48), string.char(0x44,0x49),
|
|
||||||
string.char(0x44,0x4A), string.char(0x43,0x41), string.char(0x43,0x42), string.char(0x43,0x44), string.char(0x43,0x45),
|
|
||||||
string.char(0x43,0x46), string.char(0x43,0x47), string.char(0x43,0x48), string.char(0x43,0x49), string.char(0x43,0x4A),
|
|
||||||
string.char(0x43,0x4B), string.char(0x43,0x4C), string.char(0x43,0x4D), string.char(0x43,0x4E), string.char(0x43,0x4F),
|
|
||||||
string.char(0x44,0x4E), string.char(0x44,0x4B), string.char(0x44,0x4C), string.char(0x45,0x41), string.char(0x46,0x4F),
|
|
||||||
string.char(0x46,0x50), string.char(0x48,0x4C), string.char(0x48,0x4E), string.char(0x48,0x4F)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln = string.len(name)
|
|
||||||
y = 1
|
|
||||||
|
|
||||||
while (y <= ln) do
|
|
||||||
temp = string.byte(name, y)
|
|
||||||
|
|
||||||
if (temp == 0x00) then --Sharename must be followed by spaces not null's to be acceptable
|
|
||||||
return(nil)
|
|
||||||
elseif (temp == '"') then
|
|
||||||
nchar = string.char(0x43,0x43)
|
|
||||||
else do
|
|
||||||
temp = string.char(temp)
|
|
||||||
pos = string.find(chars, temp)
|
|
||||||
nchar = trtable[pos]
|
|
||||||
if (y > 1) then
|
|
||||||
newname = newname .. nchar
|
|
||||||
else
|
|
||||||
newname = nchar
|
|
||||||
end
|
|
||||||
y = y + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return(newname)
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- This function invokes the TCP traffic that is generated to get
|
|
||||||
-- a response that yields the OS version information. The first
|
|
||||||
-- payload is an SMB session initiation request followed by a
|
|
||||||
-- negotiate payload, and followed by a Session Setup AndX request.
|
|
||||||
-- The workstation share name extracted from the UDP wildcard NetBIOS
|
|
||||||
-- response must be used in the SMB session initiation request(payload 1).
|
|
||||||
-- Payload for the requests that follow is static.
|
|
||||||
|
|
||||||
function tcp_session(ename, host, use_raw)
|
|
||||||
|
|
||||||
local catch = function()
|
|
||||||
socket:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
local rec1_payload, rec2_payload, rec3_payload, status, line1, line2, line3, currenttime, osversion, winshare, pos, message
|
|
||||||
|
|
||||||
message = 0
|
|
||||||
local win5 = "Windows 5.0"
|
|
||||||
local win51 = "Windows 5.1"
|
|
||||||
|
|
||||||
winshare = string.char(0x20, 0x46, 0x48, 0x45, 0x4A, 0x45, 0x4F, 0x45, 0x45, 0x45, 0x50, 0x46, 0x48, 0x46, 0x44, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x00)
|
|
||||||
|
|
||||||
rec1_payload = string.char(0x81, 0x00, 0x00, 0x44) .. ename .. winshare
|
|
||||||
|
|
||||||
rec2_payload = string.char( 0x00, 0x00, 0x00, 0x85, 0xff, 0x53, 0x4d, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0 ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfa ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x17, 0x62, 0x00, 0x61, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f ) ..
|
|
||||||
string.char( 0x52, 0x4b, 0x20, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02 ) ..
|
|
||||||
string.char( 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x31, 0x2e, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6e, 0x64, 0x6f ) ..
|
|
||||||
string.char( 0x77, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x57, 0x6f, 0x72, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70 ) ..
|
|
||||||
string.char( 0x73, 0x20, 0x33, 0x2e, 0x31, 0x61, 0x00, 0x02, 0x4c, 0x4d, 0x31, 0x2e, 0x32, 0x58, 0x30, 0x30 ) ..
|
|
||||||
string.char( 0x32, 0x00, 0x02, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x32, 0x2e, 0x31, 0x00, 0x02, 0x4e, 0x54 ) ..
|
|
||||||
string.char( 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 0x32, 0x00)
|
|
||||||
|
|
||||||
rec3_payload = string.char( 0x00, 0x00, 0x00, 0xab, 0xff, 0x53, 0x4d, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0 ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfa ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x17, 0x62, 0x0d, 0xff, 0x00, 0x00, 0x00, 0x04, 0x11, 0x0a, 0x00, 0x00, 0x00, 0x00 ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x6d ) ..
|
|
||||||
string.char( 0x00, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00 ) ..
|
|
||||||
string.char( 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00 ) ..
|
|
||||||
string.char( 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x39, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x39, 0x00 ) ..
|
|
||||||
string.char( 0x38, 0x00, 0x2f, 0x00, 0x4d, 0x00, 0x65, 0x00, 0x2f, 0x00, 0x4e, 0x00, 0x54, 0x00, 0x2f, 0x00 ) ..
|
|
||||||
string.char( 0x32, 0x00, 0x6b, 0x00, 0x2f, 0x00, 0x58, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00 ) ..
|
|
||||||
string.char( 0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00, 0x65, 0x00, 0x7a, 0x00, 0x20, 0x00, 0x53, 0x00 ) ..
|
|
||||||
string.char( 0x63, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00)
|
|
||||||
|
|
||||||
local socket = nmap.new_socket()
|
|
||||||
local try = nmap.new_try(catch)
|
|
||||||
|
|
||||||
if(use_raw) then
|
|
||||||
try(socket:connect(host.ip,445,"tcp"))
|
|
||||||
else
|
|
||||||
try(socket:connect(host.ip,139,"tcp"))
|
|
||||||
end
|
|
||||||
|
|
||||||
if(not use_raw) then
|
|
||||||
socket:set_timeout(100)
|
|
||||||
try(socket:send(rec1_payload))
|
|
||||||
status, line1 = socket:receive_lines(1)
|
|
||||||
|
|
||||||
if (not status) then
|
|
||||||
socket:close()
|
|
||||||
message = "Never received a response to SMB Session Request"
|
|
||||||
return 0, 0, message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
socket:set_timeout(100)
|
|
||||||
try(socket:send(rec2_payload))
|
|
||||||
status, line2 = socket:receive_lines(1)
|
|
||||||
|
|
||||||
if (not status) then
|
|
||||||
socket:close()
|
|
||||||
message = "Never received a response to SMB Negotiate Protocol Request"
|
|
||||||
return 0, 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
currenttime, message = extract_time(line2);
|
|
||||||
|
|
||||||
-- Check for an error parsing line2
|
|
||||||
if(currenttime == 0) then
|
|
||||||
return 0, 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
socket:set_timeout(100)
|
|
||||||
try(socket:send(rec3_payload))
|
|
||||||
status, line3 = socket:receive_lines(1)
|
|
||||||
|
|
||||||
if (not status) then
|
|
||||||
socket:close()
|
|
||||||
message = "Never received a response to SMB Setup AndX Request"
|
|
||||||
return 0, currenttime, message
|
|
||||||
end
|
|
||||||
socket:close()
|
|
||||||
|
|
||||||
-- Check for an error parsing line3
|
|
||||||
osversion, message = extract_version(line3)
|
|
||||||
if (osversion ~= 0) then
|
|
||||||
pos = string.find(osversion, win5)
|
|
||||||
if (pos ~= nil) then
|
|
||||||
osversion = "Windows 2000"
|
|
||||||
else
|
|
||||||
pos = string.find(osversion, win51)
|
|
||||||
if (pos ~= nil) then
|
|
||||||
osversion = "Windows XP"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return osversion, currenttime, message
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- Response from Session Setup AndX Request (TCP payload 3)
|
|
||||||
-- Must be SMB response. Extract the OS version from it from a fixed
|
|
||||||
-- offset in the payload.
|
|
||||||
|
|
||||||
function extract_version(line)
|
|
||||||
|
|
||||||
local temp, smb, ltemp, go, x, osversion, mychar, message
|
|
||||||
|
|
||||||
smb = "SMB" .. string.char(0x73)
|
|
||||||
temp = string_concatenate(line, 6, 9)
|
|
||||||
message = 0
|
|
||||||
|
|
||||||
if (temp ~= smb) then
|
|
||||||
message = "Didn't find correct SMB record as a response to the Session Setup AndX request"
|
|
||||||
return 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
ltemp = string.len(line)
|
|
||||||
temp = string_concatenate(line, 47, ltemp)
|
|
||||||
x=1
|
|
||||||
|
|
||||||
osversion = ""
|
|
||||||
while (x < ltemp) do
|
|
||||||
mychar = string.byte(temp,x)
|
|
||||||
if (mychar == 0) then
|
|
||||||
return osversion, message
|
|
||||||
else
|
|
||||||
osversion = osversion .. string.char(mychar)
|
|
||||||
end
|
|
||||||
x = x + 2
|
|
||||||
end
|
|
||||||
|
|
||||||
if (x >= ltemp) then
|
|
||||||
message = "OS version not found in expected record Session Setup AndX response"
|
|
||||||
return 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------
|
|
||||||
-- Response from Negotiate Protocol Response (TCP payload 2)
|
|
||||||
-- Must be SMB response. Extract the time from it from a fixed
|
|
||||||
-- offset in the payload.
|
|
||||||
|
|
||||||
function extract_time(line)
|
|
||||||
|
|
||||||
local smb, tmp, message, i, timebuf, timezonebuf, time, timezone
|
|
||||||
|
|
||||||
message = 0
|
|
||||||
|
|
||||||
if(string.sub(line, 6, 8) ~= "SMB") then
|
|
||||||
message = "Didn't find correct SMB record as a response to the Negotiate Protocol Response"
|
|
||||||
return 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
if(string.byte(line, 9) ~= 0x72) then
|
|
||||||
message = "Incorrect Negotiate Protocol Response type"
|
|
||||||
return 0, message
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extract the timestamp from the response
|
|
||||||
i = 1
|
|
||||||
time = 0
|
|
||||||
timebuf = string.sub(line, 0x3d, 0x3d + 7)
|
|
||||||
while (i <= 8) do
|
|
||||||
time = time + 1.0 + (bit.lshift(string.byte(timebuf, i), 8 * (i - 1)))
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
-- Convert time from 1/10 microseconds to seconds
|
|
||||||
time = (time / 10000000) - 11644473600;
|
|
||||||
|
|
||||||
-- Extract the timezone offset from the response
|
|
||||||
timezonebuf = string.sub(line, 0x45, 0x45 + 2)
|
|
||||||
timezone = (string.byte(timezonebuf, 1) + (bit.lshift(string.byte(timezonebuf, 2), 8)))
|
|
||||||
|
|
||||||
-- This is a nasty little bit of code, so I'll explain it in detail. If the timezone has the
|
|
||||||
-- highest-order bit set, it means it was negative. If so, we want to take the two's complement
|
|
||||||
-- of it (not(x)+1) and divide by 60, to get minutes. Otherwise, just divide by 60.
|
|
||||||
-- To further complicate things (as if we needed _that_!), the timezone offset is the number of
|
|
||||||
-- minutes you'd have to add to the time to get to UTC, so it's actually the negative of what
|
|
||||||
-- we want. Confused yet?
|
|
||||||
if(timezone == 0x00) then
|
|
||||||
timezone = "UTC+0"
|
|
||||||
elseif(bit.band(timezone, 0x8000) == 0x8000) then
|
|
||||||
timezone = "UTC+" .. ((bit.band(bit.bnot(timezone), 0x0FFFF) + 1) / 60)
|
|
||||||
else
|
|
||||||
timezone = "UTC-" .. (timezone / 60)
|
|
||||||
end
|
|
||||||
|
|
||||||
return (os.date("%Y-%m-%d %H:%M:%S", time) .. " " .. timezone), message;
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,94 +1,98 @@
|
|||||||
Entry{ category = "default", filename = "dns-test-open-recursion.nse" }
|
Entry{ category = "default", filename = "showOwner.nse" }
|
||||||
Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" }
|
Entry{ category = "safe", filename = "showOwner.nse" }
|
||||||
|
Entry{ category = "demo", filename = "daytimeTest.nse" }
|
||||||
Entry{ category = "default", filename = "RealVNC_auth_bypass.nse" }
|
Entry{ category = "default", filename = "RealVNC_auth_bypass.nse" }
|
||||||
Entry{ category = "malware", filename = "RealVNC_auth_bypass.nse" }
|
Entry{ category = "malware", filename = "RealVNC_auth_bypass.nse" }
|
||||||
Entry{ category = "vuln", filename = "RealVNC_auth_bypass.nse" }
|
Entry{ category = "vuln", filename = "RealVNC_auth_bypass.nse" }
|
||||||
Entry{ category = "external", filename = "dns-safe-recursion-port.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" }
|
|
||||||
Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" }
|
|
||||||
Entry{ category = "default", filename = "showOwner.nse" }
|
|
||||||
Entry{ category = "safe", filename = "showOwner.nse" }
|
|
||||||
Entry{ category = "default", filename = "SSLv2-support.nse" }
|
|
||||||
Entry{ category = "safe", filename = "SSLv2-support.nse" }
|
|
||||||
Entry{ category = "malware", filename = "ircZombieTest.nse" }
|
|
||||||
Entry{ category = "version", filename = "skype_v2-version.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "HTTPtrace.nse" }
|
|
||||||
Entry{ category = "demo", filename = "echoTest.nse" }
|
|
||||||
Entry{ category = "default", filename = "UPnP-info.nse" }
|
|
||||||
Entry{ category = "safe", filename = "UPnP-info.nse" }
|
|
||||||
Entry{ category = "default", filename = "rpcinfo.nse" }
|
|
||||||
Entry{ category = "safe", filename = "rpcinfo.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "rpcinfo.nse" }
|
|
||||||
Entry{ category = "auth", filename = "bruteTelnet.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "bruteTelnet.nse" }
|
|
||||||
Entry{ category = "external", filename = "dns-safe-recursion-txid.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" }
|
|
||||||
Entry{ category = "default", filename = "SMTPcommands.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "SMTPcommands.nse" }
|
|
||||||
Entry{ category = "safe", filename = "SMTPcommands.nse" }
|
|
||||||
Entry{ category = "default", filename = "robots.nse" }
|
|
||||||
Entry{ category = "safe", filename = "robots.nse" }
|
|
||||||
Entry{ category = "default", filename = "zoneTrans.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "zoneTrans.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "zoneTrans.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "whois.nse" }
|
|
||||||
Entry{ category = "external", filename = "whois.nse" }
|
|
||||||
Entry{ category = "safe", filename = "whois.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "ripeQuery.nse" }
|
|
||||||
Entry{ category = "external", filename = "ripeQuery.nse" }
|
|
||||||
Entry{ category = "demo", filename = "chargenTest.nse" }
|
|
||||||
Entry{ category = "malware", filename = "strangeSMTPport.nse" }
|
|
||||||
Entry{ category = "version", filename = "iax2Detect.nse" }
|
|
||||||
Entry{ category = "demo", filename = "showSMTPVersion.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "ASN.nse" }
|
|
||||||
Entry{ category = "external", filename = "ASN.nse" }
|
|
||||||
Entry{ category = "default", filename = "showHTMLTitle.nse" }
|
|
||||||
Entry{ category = "demo", filename = "showHTMLTitle.nse" }
|
|
||||||
Entry{ category = "safe", filename = "showHTMLTitle.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "promiscuous.nse" }
|
|
||||||
Entry{ category = "version", filename = "netbios-smb-os-discovery.nse" }
|
|
||||||
Entry{ category = "default", filename = "anonFTP.nse" }
|
|
||||||
Entry{ category = "auth", filename = "anonFTP.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "anonFTP.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "SQLInject.nse" }
|
Entry{ category = "intrusive", filename = "SQLInject.nse" }
|
||||||
Entry{ category = "vuln", filename = "SQLInject.nse" }
|
Entry{ category = "vuln", filename = "SQLInject.nse" }
|
||||||
|
Entry{ category = "auth", filename = "bruteTelnet.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "bruteTelnet.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "HTTPtrace.nse" }
|
||||||
Entry{ category = "demo", filename = "SMTP_openrelay_test.nse" }
|
Entry{ category = "demo", filename = "SMTP_openrelay_test.nse" }
|
||||||
Entry{ category = "default", filename = "nbstat.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "nbstat.nse" }
|
|
||||||
Entry{ category = "safe", filename = "nbstat.nse" }
|
|
||||||
Entry{ category = "default", filename = "HTTPAuth.nse" }
|
Entry{ category = "default", filename = "HTTPAuth.nse" }
|
||||||
Entry{ category = "auth", filename = "HTTPAuth.nse" }
|
Entry{ category = "auth", filename = "HTTPAuth.nse" }
|
||||||
Entry{ category = "intrusive", filename = "HTTPAuth.nse" }
|
Entry{ category = "intrusive", filename = "HTTPAuth.nse" }
|
||||||
Entry{ category = "default", filename = "finger.nse" }
|
Entry{ category = "default", filename = "dns-test-open-recursion.nse" }
|
||||||
Entry{ category = "discovery", filename = "finger.nse" }
|
Entry{ category = "intrusive", filename = "dns-test-open-recursion.nse" }
|
||||||
Entry{ category = "demo", filename = "showHTTPVersion.nse" }
|
Entry{ category = "demo", filename = "chargenTest.nse" }
|
||||||
Entry{ category = "default", filename = "SSHv1-support.nse" }
|
Entry{ category = "default", filename = "showHTMLTitle.nse" }
|
||||||
Entry{ category = "safe", filename = "SSHv1-support.nse" }
|
Entry{ category = "demo", filename = "showHTMLTitle.nse" }
|
||||||
Entry{ category = "default", filename = "popcapa.nse" }
|
Entry{ category = "safe", filename = "showHTMLTitle.nse" }
|
||||||
Entry{ category = "default", filename = "SNMPsysdescr.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "SNMPsysdescr.nse" }
|
|
||||||
Entry{ category = "safe", filename = "SNMPsysdescr.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "brutePOP3.nse" }
|
|
||||||
Entry{ category = "auth", filename = "brutePOP3.nse" }
|
|
||||||
Entry{ category = "default", filename = "MySQLinfo.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "MySQLinfo.nse" }
|
|
||||||
Entry{ category = "safe", filename = "MySQLinfo.nse" }
|
|
||||||
Entry{ category = "default", filename = "ftpbounce.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "ftpbounce.nse" }
|
|
||||||
Entry{ category = "auth", filename = "xamppDefaultPass.nse" }
|
|
||||||
Entry{ category = "vuln", filename = "xamppDefaultPass.nse" }
|
|
||||||
Entry{ category = "intrusive", filename = "HTTPpasswd.nse" }
|
|
||||||
Entry{ category = "vuln", filename = "HTTPpasswd.nse" }
|
|
||||||
Entry{ category = "demo", filename = "showSSHVersion.nse" }
|
|
||||||
Entry{ category = "version", filename = "PPTPversion.nse" }
|
|
||||||
Entry{ category = "default", filename = "ircServerInfo.nse" }
|
|
||||||
Entry{ category = "discovery", filename = "ircServerInfo.nse" }
|
|
||||||
Entry{ category = "default", filename = "MSSQLm.nse" }
|
Entry{ category = "default", filename = "MSSQLm.nse" }
|
||||||
Entry{ category = "discovery", filename = "MSSQLm.nse" }
|
Entry{ category = "discovery", filename = "MSSQLm.nse" }
|
||||||
Entry{ category = "intrusive", filename = "MSSQLm.nse" }
|
Entry{ category = "intrusive", filename = "MSSQLm.nse" }
|
||||||
|
Entry{ category = "demo", filename = "echoTest.nse" }
|
||||||
|
Entry{ category = "default", filename = "SSHv1-support.nse" }
|
||||||
|
Entry{ category = "safe", filename = "SSHv1-support.nse" }
|
||||||
|
Entry{ category = "default", filename = "MySQLinfo.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "MySQLinfo.nse" }
|
||||||
|
Entry{ category = "safe", filename = "MySQLinfo.nse" }
|
||||||
|
Entry{ category = "auth", filename = "xamppDefaultPass.nse" }
|
||||||
|
Entry{ category = "vuln", filename = "xamppDefaultPass.nse" }
|
||||||
|
Entry{ category = "default", filename = "SSLv2-support.nse" }
|
||||||
|
Entry{ category = "safe", filename = "SSLv2-support.nse" }
|
||||||
|
Entry{ category = "default", filename = "zoneTrans.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "zoneTrans.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "zoneTrans.nse" }
|
||||||
|
Entry{ category = "default", filename = "ftpbounce.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "ftpbounce.nse" }
|
||||||
|
Entry{ category = "version", filename = "skype_v2-version.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "promiscuous.nse" }
|
||||||
|
Entry{ category = "default", filename = "SNMPsysdescr.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "SNMPsysdescr.nse" }
|
||||||
|
Entry{ category = "safe", filename = "SNMPsysdescr.nse" }
|
||||||
|
Entry{ category = "demo", filename = "showSMTPVersion.nse" }
|
||||||
|
Entry{ category = "default", filename = "nbstat.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "nbstat.nse" }
|
||||||
|
Entry{ category = "safe", filename = "nbstat.nse" }
|
||||||
|
Entry{ category = "version", filename = "iax2Detect.nse" }
|
||||||
|
Entry{ category = "default", filename = "rpcinfo.nse" }
|
||||||
|
Entry{ category = "safe", filename = "rpcinfo.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "rpcinfo.nse" }
|
||||||
Entry{ category = "default", filename = "HTTP_open_proxy.nse" }
|
Entry{ category = "default", filename = "HTTP_open_proxy.nse" }
|
||||||
Entry{ category = "discovery", filename = "HTTP_open_proxy.nse" }
|
Entry{ category = "discovery", filename = "HTTP_open_proxy.nse" }
|
||||||
Entry{ category = "external", filename = "HTTP_open_proxy.nse" }
|
Entry{ category = "external", filename = "HTTP_open_proxy.nse" }
|
||||||
Entry{ category = "intrusive", filename = "HTTP_open_proxy.nse" }
|
Entry{ category = "intrusive", filename = "HTTP_open_proxy.nse" }
|
||||||
Entry{ category = "demo", filename = "daytimeTest.nse" }
|
Entry{ category = "intrusive", filename = "HTTPpasswd.nse" }
|
||||||
|
Entry{ category = "vuln", filename = "HTTPpasswd.nse" }
|
||||||
|
Entry{ category = "demo", filename = "showSSHVersion.nse" }
|
||||||
|
Entry{ category = "default", filename = "SMTPcommands.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "SMTPcommands.nse" }
|
||||||
|
Entry{ category = "safe", filename = "SMTPcommands.nse" }
|
||||||
|
Entry{ category = "default", filename = "anonFTP.nse" }
|
||||||
|
Entry{ category = "auth", filename = "anonFTP.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "anonFTP.nse" }
|
||||||
|
Entry{ category = "default", filename = "robots.nse" }
|
||||||
|
Entry{ category = "safe", filename = "robots.nse" }
|
||||||
|
Entry{ category = "default", filename = "finger.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "finger.nse" }
|
||||||
|
Entry{ category = "default", filename = "UPnP-info.nse" }
|
||||||
|
Entry{ category = "safe", filename = "UPnP-info.nse" }
|
||||||
|
Entry{ category = "malware", filename = "strangeSMTPport.nse" }
|
||||||
|
Entry{ category = "default", filename = "ircServerInfo.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "ircServerInfo.nse" }
|
||||||
|
Entry{ category = "malware", filename = "ircZombieTest.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "ripeQuery.nse" }
|
||||||
|
Entry{ category = "external", filename = "ripeQuery.nse" }
|
||||||
|
Entry{ category = "demo", filename = "showHTTPVersion.nse" }
|
||||||
|
Entry{ category = "version", filename = "PPTPversion.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "ASN.nse" }
|
||||||
|
Entry{ category = "external", filename = "ASN.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "brutePOP3.nse" }
|
||||||
|
Entry{ category = "auth", filename = "brutePOP3.nse" }
|
||||||
|
Entry{ category = "default", filename = "popcapa.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "SNMPcommunitybrute.nse" }
|
||||||
|
Entry{ category = "auth", filename = "SNMPcommunitybrute.nse" }
|
||||||
|
Entry{ category = "discovery", filename = "whois.nse" }
|
||||||
|
Entry{ category = "external", filename = "whois.nse" }
|
||||||
|
Entry{ category = "safe", filename = "whois.nse" }
|
||||||
|
Entry{ category = "external", filename = "dns-safe-recursion-txid.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "dns-safe-recursion-txid.nse" }
|
||||||
|
Entry{ category = "version", filename = "smb-enum.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "smb-enum.nse" }
|
||||||
|
Entry{ category = "external", filename = "dns-safe-recursion-port.nse" }
|
||||||
|
Entry{ category = "intrusive", filename = "dns-safe-recursion-port.nse" }
|
||||||
|
Entry{ category = "version", filename = "smb-os-discovery.nse" }
|
||||||
|
Entry{ category = "default", filename = "smb-os-discovery.nse" }
|
||||||
|
Entry{ category = "version", filename = "smb-security-mode.nse" }
|
||||||
|
|||||||
197
scripts/smb-enum.nse
Normal file
197
scripts/smb-enum.nse
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
--- 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
|
||||||
|
|
||||||
|
|
||||||
80
scripts/smb-os-discovery.nse
Normal file
80
scripts/smb-os-discovery.nse
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
--- 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
|
||||||
|
|
||||||
|
|
||||||
112
scripts/smb-security-mode.nse
Normal file
112
scripts/smb-security-mode.nse
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
--- 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
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user