|
Nmap Development
mailing list archives
NSELib RFC -- NetBIOS / SMB
From: Ron <ron () skullsecurity net>
Date: Mon, 08 Sep 2008 21:34:57 -0500
Hey guys,
As promised, I took apart smb-probe.nse and turned into a couple nselib
libraries. For anybody who hasn't done nselibs before, I'll just say for
the record -- it's extremely easy!
Anyways, I included smb-probe.nse for now, and it still works as it did
before, but it's only a demonstration of what smb.lua and netbios.lua
do. I'm going to re-write it as a handful of smaller and more targeted
scripts.
So anyways, any thoughts on how this looks, or how it's set up? I'll
post a couple useful scripts later this week, hopefully.
Also, what's the best format to submit nselibs in, raw files or a patch?
Thanks,
Ron
--- 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'
--- 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)
-- Truncate or pad the string to 16 bytes
if(string.len(name) > 16) then
name = string.sub(name, 1, 16)
else
local padding = " "
if name == "*" then
padding = "\0"
end
repeat
name = name .. padding
until string.len(name) == 16
end
-- Do the L1 encoding
local L1_encoded = ""
for i=1, string.len(name), 1 do
local b = string.byte(name, i)
L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0xF0), 4) + 0x41)
L1_encoded = L1_encoded .. string.char(bit.rshift(bit.band(b, 0x0F), 0) + 0x41)
end
-- Do the L2 encoding
local L2_encoded = string.char(32) .. L1_encoded
if scope ~= nil then
-- Split the scope at its periods
local piece
for piece in string.gmatch(scope, "[^.]+") do
L2_encoded = L2_encoded .. string.char(string.len(piece)) .. piece
end
end
return L2_encoded
end
--- Does the exact opposite of 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
for i = 2, len + 1, 2 do
local ch = 0
ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i) - 0x41, 4))
ch = bit.bor(ch, bit.lshift(string.byte(encoded_name, i + 1) - 0x41, 0))
name = name .. string.char(ch)
end
-- Decode the scope
local pos = 34
while string.len(encoded_name) > pos do
local len = string.byte(encoded_name, pos)
scope = scope .. string.sub(encoded_name, pos + 1, pos + len) .. "."
pos = pos + 1 + len
end
-- If there was a scope, remove the trailing period
if(string.len(scope) > 0) then
scope = string.sub(scope, 1, string.len(scope) - 1)
end
return name, scope
end
--- Sends out a UDP probe on port 137 to get a human-readable list of names the
-- the system is using.
-- () param host The IP or hostname to check.
-- () param prefix [optional] The prefix to put on each line when it's returned.
-- () return (status, result) If status is true, the result is a human-readable
-- list of names. Otherwise, result is an error message.
function get_names(host, prefix)
local status, names, name_count = do_nbstat(host)
if(prefix == nil) then
prefix = ""
end
if(status) then
local result = ""
for i = 1, name_count, 1 do
result = result .. string.format("%s%s<%02x>\n", prefix, names[i][1], names[i][2])
end
return true, result
else
return false, names
end
end
--- Sends out a UDP probe on port 137 to get the server's name (that is, the
-- entry in its NBSTAT table with a 0x20 suffix).
-- () param host The IP or hostname of the server.
-- () return (status, result) If status is true, the result is the NetBIOS name.
-- otherwise, result is an error message.
function get_server_name(host)
local status, names, name_count = do_nbstat(host)
if(status) then
local i
for i = 1, name_count, 1 do
if names[i][2] == 0x20 then
return true, names[i][1]
end
end
else
return false, names
end
return false, "Couldn't find NetBIOS server name"
end
--- This is the function that actually handles the UDP query to retrieve
-- the NBSTAT information.
--
-- The NetBIOS request's header looks like this:
-- --------------------------------------------------\n
-- | 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |\n
-- | NAME_TRN_ID |\n
-- | R | OPCODE | NM_FLAGS | RCODE | (FLAGS)\n
-- | QDCOUNT |\n
-- | ANCOUNT |\n
-- | NSCOUNT |\n
-- | ARCOUNT |\n
-- --------------------------------------------------\n
--
-- In this case, the TRN_ID is a constant (0x1337, what else?), the flags
-- are 0, and we have one question. All fields are network byte order.
--
-- The body of the packet is a list of names to check for in the following
-- format:
-- (ntstring) encoded name
-- (2 bytes) query type (0x0021 = NBSTAT)
-- (2 bytes) query class (0x0001 = IN)
--
-- The response header is the exact same, except it'll have some flags set
-- (0x8000 for sure, since it's a response), and ANCOUNT will be 1. The format
-- of the answer is:\n
-- (ntstring) requested name\n
-- (2 bytes) query type\n
-- (2 bytes) query class\n
-- (2 bytes) time to live\n
-- (2 bytes) record length\n
-- (1 byte) number of names\n
-- [for each name]\n
-- (16 bytes) padded name, with a 1-byte suffix\n
-- (2 bytes) flags\n
--
-- () param host The IP or hostname of the system.
-- () return (status, result) If status is true, then the servers names are
-- returned as an array of (name, suffix, flags).
-- Otherwise, result is an error message.
function do_nbstat(host)
local socket = nmap.new_socket()
local encoded_name = name_encode("*")
-- Create the query header
local query = bin.pack(">SSSSSS",
0x1337, -- Transaction id
0x0000, -- Flags
1, -- Questions
0, -- Answers
0, -- Authority
0 -- Extra
)
query = query .. bin.pack(">zSS",
encoded_name, -- Encoded name
0x0021, -- Query type (0x21 = NBSTAT)
0x0001 -- Class = IN
)
socket:connect(host, 137, "udp")
socket:send(query)
socket:set_timeout(1000)
local status, result = socket:receive_bytes(1);
socket:close()
if(status) then
local pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT, rr_name, rr_type, rr_class, rr_ttl
local rrlength, name_count
pos, TRN_ID, FLAGS, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT = bin.unpack(">SSSSSS", result)
-- Sanity check the result (has to have the same TRN_ID, 1 answer, and proper flags)
if(TRN_ID ~= 0x1337) then
return false, string.format("Invalid transaction ID returned: 0x%04x", TRN_ID)
end
if(ANCOUNT ~= 1) then
return false, "Server returned an invalid number of answers"
end
if(bit.band(FLAGS, 0x8000) == 0) then
return false, "Server's flags didn't indicate a response"
end
if(bit.band(FLAGS, 0x0007) ~= 0) then
return false, string.format("Server returned a NetBIOS error: 0x%02x", bit.band(FLAGS, 0x0007))
end
-- Start parsing the answer field
pos, rr_name, rr_type, rr_class, rr_ttl = bin.unpack(">zSSI", result, pos)
-- More sanity checks
if(rr_name ~= encoded_name) then
return false, "Server returned incorrect name"
end
if(rr_class ~= 0x0001) then
return false, "Server returned incorrect class"
end
if(rr_type ~= 0x0021) then
return false, "Server returned incorrect query type"
end
pos, rrlength, name_count = bin.unpack(">SC", result, pos)
local names = {}
for i = 1, name_count do
local name, suffix, flags
-- Instead of reading the 16-byte name and pulling off the suffix,
-- we read the first 15 bytes and then the 1-byte suffix.
pos, name, suffix, flags = bin.unpack(">A15CS", result, pos)
name = string.gsub(name, "[ ]*$", "")
names[i] = { name, suffix, flags }
end
return true, names, name_count
else
return false, "Name query failed: " .. result
end
end
--- 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:
--
-- 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
--
-- 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'
--- 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().
-- [TODO] error handling
--
-- () param host The host (or IP) 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, result is the error message.
function start_raw(host, port)
local socket = nmap.new_socket()
socket:connect(host, port, "tcp")
return true, socket
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.
--
-- () param host The host (or IP) 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, port) if status is true, result is the port
-- Otherwise, result is the error message.
function start_netbios(host, port, name)
local pos, status, result, flags, length
local socket = nmap.new_socket()
if(name == nil) then
-- Get the name of the server
status, name = netbios.get_server_name(host)
if(status == false) then
return false, result
end
end
-- Request a NetBIOS session
session_request = bin.pack(">CCSzz",
0x81, -- session request
0x00, -- flags
0x44, -- length
netbios.name_encode(name), -- server name
netbios.name_encode("NMAP") -- client name
);
socket:connect(host, port, "tcp")
socket:send(session_request)
socket:set_timeout(1000)
-- Receive the session response
status, result = socket:receive_bytes(4);
pos, result, flags, length = bin.unpack(">CCS", result)
-- Check for a position session response (0x82)
if result ~= 0x82 then
return false, "Server refused to grant a NetBIOS session"
end
return true, socket
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.
-- [TODO] Error checking
--
-- () 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
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)
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)
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 smb_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
smb_send(socket, header, parameters, data)
-- Read the result
status, header, parameters, data = smb_read(socket)
if(status ~= true) then
return false, header
end
-- Since this is our first response, parse out the header
pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid,
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)
-- Parse the parameter section
pos, dialect, security_mode, max_mpx, max_vc, max_buffer, max_raw_buffer, session_key, capabilities, time,
timezone, key_length = bin.unpack("<SCSSIIIILsC", parameters)
-- 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 smb_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
smb_send(socket, header, parameters, data)
-- Read the result
status, header, parameters, data = smb_read(socket)
if(status ~= true) then
return false, header
end
-- Check if we were allowed in
pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid,
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)
if(status ~= 0) then
return false, status
end
-- Parse the parameters
pos, andx_command, andx_reserved, andx_offset, action = bin.unpack("<CCSS", parameters)
-- Parse the data
pos, os, lanmanager, domain = bin.unpack("<zzz", data)
-- 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 smb_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
smb_send(socket, header, parameters, data)
-- Read the result
status, header, parameters, data = smb_read(socket)
if(status ~= true) then
return false, header
end
-- Check if we were allowed in
pos, header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, tid, pid,
uid, mid = bin.unpack("<CCCCCICSSlSSSSS", header)
if(status ~= 0) then
return false, status
end
response['tid'] = tid
return true, response
end
--- Probes a system running SMB (CIFS) for more information.
--
-- Currently serving only as a demonstration stripped, will be re-tasked
-- shortly.
--
-- () usage
-- nmap --script smb-probe.nse -p445 127.0.0.1\n
-- sudo nmap -sU -sS --script smb-probe.nse -p U:137,T:139 127.0.0.1\n
--
-- () output
-- Against a weak box:
-- Host script results:\n
-- | Probe SMB for information: \n
-- | SMB Security: User-level authentication\n
-- | SMB Security: Challenge/response passwords supported\n
-- | SMB Security: Message signing supported\n
-- | System time from SMB: 2008-09-07 00:56:02 [UTC-5]\n
-- | Computer name from SMB: WORKGROUP\TEST1\n
-- | OS detection from SMB: Windows 2000\n
-- | Null sessions enabled\n
-- | Found a share 'TEST'\n
-- | Found a share 'TEST$'\n
-- | Guest account enabled\n
-- |_ Guest account has access to share 'TEST'\n
--\n
-- Against a more locked down host:\n
-- Host script results:\n
-- | Probe SMB for information: \n
-- | SMB Security: User-level authentication\n
-- | SMB Security: Challenge/response passwords supported\n
-- | SMB Security: Message signing required\n
-- | System time from SMB: 2008-09-07 01:55:46 [UTC-5]\n
-- | Computer name from SMB: WORKGROUP\TEST2\n
-- | OS detection from SMB: Windows 2000\n
-- | Null sessions disabled\n
-- |_ Guest account disabled\n
--
-----------------------------------------------------------------------
id = "Probe SMB for information"
description = "Elicits information from a host running NetBIOS/SMB"
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"version","intrusive"}
require 'bit'
require 'bin'
require 'netbios'
require 'smb'
-- The address being scanned
local address
-- Shares to try connecting to as Null session / GUEST
local shares = {"C", "D", "TEST", "SHARE", "HOME"}
--- Check whether or not this script should be fun. This script should
-- run under two different conditions:
-- a) port tcp/445 is open (allowing us to make a raw connection)\n
-- b) ports tcp/139 and udp/137 are open (137 may not be known)\n
hostrule = function(host)
local port_u137 = nmap.get_port_state(host, {number=137, protocol="udp"})
local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"})
local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})
if(port_t445 ~= nil and port_t445.state == "open") then
-- tcp/445 is open, we're good
return true
end
if(port_t139 ~= nil and port_t139.state == "open") then
-- tcp/139 is open, check uf udp/137 is open or unknown
if(port_u137 == nil or port_u137.state == "open" or port_u137.state == "open|filtered") then
return true
end
end
return false
end
--- Calls the functions to send out the packets, and puts together most of the results.
-- This is basically the core function in this script, and where most of the future
-- additions will happen.
--
-- Currently, it does the following:\n
-- * Sends out SMB_COM_NEGOTIATE_PROTOCOL\n
-- * Starts a NULL session\n
-- * Parses the OS\n
-- * Tries to connect to shares\n
-- * Tries to start a GUEST session\n
-- * Tries to connect to shares\n
-- * Tries to start an Administrator session (with a blank password)\n
-- () param socket The socket to use for this connection (it is assumed that the function
-- can go ahead and start sending SMB traffic, so if the socket requires any kind
-- of startup, it has to be done already.
function smb_start(host, port)
local status, result
local response = ""
local os = ""
local lanmanager = ""
if(port == 445) then
result, socket = smb.start_raw(host, port)
else
result, socket = smb.start_netbios(host, port)
end
if(result == false) then
return false, socket
end
-- Negotiate protocol
status, result = smb.smb_negotiate_protocol(socket)
if(status == false) then
return false, result
end
response = response .. string.format("Computer name from SMB: %s\\%s\n", result['domain'], result['server'])
response = response .. string.format("System time from SMB: %s [%s]\n", result['date'], result['timezone_str'])
-- Check the security mode
if(bit.band(result['security_mode'], 1) == 1) then
response = response .. "SMB Security: User-level authentication\n"
else
response = response .. "SMB Security: Share-level authentication\n"
end
-- Challenge/response supported?
if(bit.band(result['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(result['security_mode'], 8) == 8) then
response = response .. "SMB Security: Message signing required\n"
elseif(bit.band(result['security_mode'], 4) == 4) then
response = response .. "SMB Security: Message signing supported\n"
else
response = response .. "SMB Security: Message signing not supported\n"
end
-- Start a null session
status, result2 = smb.smb_start_session(socket, "", result['session_key'], result['capabilities'])
if(status == true) then
response = response .. string.format("OS detection from SMB: %s\n", get_windows_version(result2['os']))
response = response .. string.format("LAN Manager from SMB: %s\n", result2['lanmanager'])
-- See if it can connect to "IPC$"
status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address), result2['uid'])
if(status == true) then
response = response .. "Null sessions enabled\n"
else
response = response .. "Null sessions disabled\n"
end
-- Loop through a couple common shares, see if Null session has access
for i,share in ipairs(shares) do
-- Try and connect
status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share),
result2['uid'])
if(status == true) then
response = response .. string.format("Null account has access to share '%s'\n", share)
elseif(result3 == 0xc0000022) then -- STATUS_ACCESS_DENIED
response = response .. string.format("Found a share '%s'\n", share)
end
-- Try with a '$' on the end
status, result3 = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share),
result2['uid'])
if(status == true) then
response = response .. string.format("Null account has access to share '%s$'\n", share)
elseif(result3 == 0xc0000022) then -- STATUS_ACCESS_DENIED
response = response .. string.format("Found a share '%s$'\n", share)
end
end
end
-- Start a guest session
status, result2 = smb.smb_start_session(socket, "GUEST", result['session_key'], result['capabilities'])
if(status == true) then
-- See if it can connect to "IPC$"
status = smb.smb_tree_connect(socket, string.format("\\\\%s\\IPC$", address), result2['uid'])
if(status == true) then
response = response .. "Guest account enabled\n"
else
response = response .. "Guest account disabled\n"
end
-- Loop through a couple common shares, see if GUEST has access
for i,share in ipairs(shares) do
-- Try and connect
status = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s", address, share),
result2['uid'])
if(status == true) then
response = response .. string.format("Guest account has access to share '%s'\n", share)
end
-- Try with a '$' on the end
status = smb.smb_tree_connect(socket, string.format("\\\\%s\\%s$", address, share),
result2['uid'])
if(status == true) then
response = response .. string.format("Guest account has access to share '%s$'\n", share)
end
end
else
response = response .. "Guest account disabled\n"
end
-- Check if 'Administrator' has a blank password
status, result2 = smb.smb_start_session(socket, "ADMINISTRATOR", result['session_key'], result['capabilities'])
if(status == true) then
response = response .. "Administrator account has a blank password\n"
elseif(result2 == 0xc000006e) then -- STATUS_ACCOUNT_RESTRICTION
response = response .. "Administrator account has a blank password (but can't use SMB)"
end
return true, response
end
--- Converts numbered Windows versions (5.0, 5.1) to the names (Windows 2000, Windows XP).
-- () param os The name of the OS
-- () return The actual name of the OS (or the same as the 'os' parameter)
function get_windows_version(os)
if(os == "Windows 5.0") then
return "Windows 2000"
elseif(os == "Windows 5.1")then
return "Windows XP"
end
return os
end
action = function(host)
local port_t139 = nmap.get_port_state(host, {number=139, protocol="tcp"})
local port_t445 = nmap.get_port_state(host, {number=445, protocol="tcp"})
address = host.ip
if(port_t445 ~= nil and port_t445.state == "open") then
status, result = smb_start(host.ip, 445)
else
status, result = smb_start(host.ip, 139)
end
return string.format(" \n%s", result)
end
_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org
By Date
By Thread
Current thread:
- NSELib RFC -- NetBIOS / SMB Ron (Sep 08)
|