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