From 354eaca0ffe29bd60149ffd354a1dc23e1d4a38d Mon Sep 17 00:00:00 2001 From: ron Date: Fri, 22 Jan 2010 04:43:51 +0000 Subject: [PATCH] Re-wrote smb-enum-domains.nse to be more generic and rely on library functions. Ultimately, I want to use these same functions in smb-brute to trim out impossible passwords and figure out account lockout policies before the bruteforce starts. It'll make smb-brute a little cleaner after the changes. --- nselib/msrpc.lua | 235 +++++++++++++++++++++++++++++- scripts/smb-enum-domains.nse | 270 +++++++---------------------------- 2 files changed, 284 insertions(+), 221 deletions(-) diff --git a/nselib/msrpc.lua b/nselib/msrpc.lua index 443474bf2..a60952687 100644 --- a/nselib/msrpc.lua +++ b/nselib/msrpc.lua @@ -2621,8 +2621,8 @@ function svcctl_openscmanagerw(smbstate, machinename) arguments = arguments .. msrpctypes.marshall_unicode_ptr(nil, true) -- [in] uint32 access_mask, - arguments = arguments .. msrpctypes.marshall_int32(0x000f003f) --- arguments = arguments .. msrpctypes.marshall_int32(0x00000002) +-- arguments = arguments .. msrpctypes.marshall_int32(0x000f003f) + arguments = arguments .. msrpctypes.marshall_int32(0x02000000) -- [out,ref] policy_handle *handle @@ -3664,6 +3664,237 @@ function get_user_list(host) return true, response, names end +---Retrieve information about a domain. This is done by three seperate calls to samr_querydomaininfo2() to get all +-- possible information. smbstate has to be in the proper state for this to work. +local function get_domain_info(host, domain) + local result = {} + local status, smbstate, bind_result, connect4_result, lookupdomain_result, opendomain_result, enumdomainusers_result + + -- Create the SMB session + status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH) + if(status == false) then + return 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) + return false, bind_result + end + + -- Call connect4() + status, connect4_result = msrpc.samr_connect4(smbstate, host.ip) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, connect4_result + end + + -- Call LookupDomain() + status, lookupdomain_result = msrpc.samr_lookupdomain(smbstate, connect4_result['connect_handle'], domain) + if(status == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, "Couldn't look up the domain: " .. lookupdomain_result + end + + -- Call OpenDomain() + status, opendomain_result = msrpc.samr_opendomain(smbstate, connect4_result['connect_handle'], lookupdomain_result['sid']) + if(status == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, opendomain_result + end + + -- Call QueryDomainInfo2() to get domain properties. We call these for three types -- 1, 8, and 12, since those return + -- the most useful information. + local status_1, querydomaininfo2_result_1 = msrpc.samr_querydomaininfo2(smbstate, opendomain_result['domain_handle'], 1) + local status_8, querydomaininfo2_result_8 = msrpc.samr_querydomaininfo2(smbstate, opendomain_result['domain_handle'], 8) + local status_12, querydomaininfo2_result_12 = msrpc.samr_querydomaininfo2(smbstate, opendomain_result['domain_handle'], 12) + + if(status_1 == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, querydomaininfo2_result_1 + end + + if(status_8 == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, querydomaininfo2_result_8 + end + + if(status_12 == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, thenquerydomaininfo2_result_12 + end + + -- Call EnumDomainUsers() to get users + status, enumdomainusers_result = msrpc.samr_enumdomainusers(smbstate, opendomain_result['domain_handle']) + if(status == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, enumdomainusers_result + end + + -- Call EnumDomainAliases() to get groups + local status, enumdomaingroups_result = msrpc.samr_enumdomainaliases(smbstate, opendomain_result['domain_handle']) + if(status == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + return false, enumdomaingroups_result + end + + -- Close the domain handle + msrpc.samr_close(smbstate, opendomain_result['domain_handle']) + -- Close the smb session + msrpc.stop_smb(smbstate) + + -- 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'] = domain + response['sid'] = lookupdomain_result['sid'] + response['groups'] = groups + response['users'] = names + if(querydomaininfo2_result_8['info']['domain_create_time'] ~= 0) then + response['created'] = os.date("%Y-%m-%d %H:%M:%S", querydomaininfo2_result_8['info']['domain_create_time']) + else + response['created'] = "unknown" + end + + -- Password characteristics + response['min_password_length'] = querydomaininfo2_result_1['info']['min_password_length'] + response['max_password_age'] = querydomaininfo2_result_1['info']['max_password_age'] / 60 / 60 / 24 + response['min_password_age'] = querydomaininfo2_result_1['info']['min_password_age'] / 60 / 60 / 24 + response['password_history'] = querydomaininfo2_result_1['info']['password_history_length'] + response['lockout_duration'] = querydomaininfo2_result_12['info']['lockout_duration'] / 60 + response['lockout_threshold'] = querydomaininfo2_result_12['info']['lockout_threshold'] + response['lockout_window'] = querydomaininfo2_result_12['info']['lockout_window'] / 60 + + -- Sanity check the different values, and remove them if they don't appear to be set + if(response['min_password_length'] <= 0) then + response['min_password_length'] = nil + end + + if(response['max_password_age'] < 0 or response['max_password_age'] > 5000) then + response['max_password_age'] = nil + end + + if(response['min_password_age'] <= 0) then + response['min_password_age'] = nil + end + + if(response['password_history'] <= 0) then + response['password_history'] = nil + end + + if(response['lockout_duration'] <= 0) then + response['lockout_duration'] = nil + end + + if(response['lockout_threshold'] <= 0) then + response['lockout_threshold'] = nil + end + + if(response['lockout_window'] <= 0) then + response['lockout_window'] = nil + 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 + + response['password_properties'] = password_properties_response + end + + return true, response +end + +function get_domains(host) + local result = {} + local status, smbstate, bind_result, connect4_result, enumdomains_result + local i, j + + -- Create the SMB session + status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH) + if(status == false) then + return 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) + return false, bind_result + end + + -- Call connect4() + status, connect4_result = msrpc.samr_connect4(smbstate, host.ip) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, connect4_result + end + + -- Call EnumDomains() + status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect4_result['connect_handle']) + if(status == false) then + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + msrpc.stop_smb(smbstate) + + return false, enumdomains_result + end + + -- Close the connect handle + msrpc.samr_close(smbstate, connect4_result['connect_handle']) + + -- Close the SMB session + msrpc.stop_smb(smbstate) + + -- If no domains were returned, return an error (not sure that this can ever happen, but who knows?) + if(#enumdomains_result['sam']['entries'] == 0) then + return false, "No domains could be found" + end + + local response = {} + for i = 1, #enumdomains_result['sam']['entries'], 1 do + local domain = enumdomains_result['sam']['entries'][i]['name'] + local status, domain_info = get_domain_info(host, domain) + + if(not(status)) then + return false, "Couldn't get info for the domain: " .. domain_info + else + response[domain] = domain_info + end + + end + + return true, response +end + ---Create a "service" on a remote machine. This service is linked to an executable that is already -- on the system. The name of the service can be whatever you want it to be. The service is created -- in the "stopped" state with "manual" startup, and it ignores errors. The 'servicename' is what diff --git a/scripts/smb-enum-domains.nse b/scripts/smb-enum-domains.nse index 5024a24d6..979e6c639 100644 --- a/scripts/smb-enum-domains.nse +++ b/scripts/smb-enum-domains.nse @@ -32,20 +32,22 @@ After the initial bind to SAMR, the sequence of calls is: -- --@output -- Host script results: --- | 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 +-- | smb-enum-domains: +-- | WINDOWS2000 +-- | Groups: n/a +-- | Users: Administrator, blah, Guest, testpass, ron, test, user +-- | Creation time: 2009-10-17 12:45:47 +-- | Passwords: min length: n/a; min age: 5 days; max age: 100 days; history: 10 passwords +-- | Properties: Complexity requirements exist +-- | Account lockout: 5 attempts in 30 minutes will lock out the account for 30 minutes +-- | Builtin +-- | Groups: Administrators, Backup Operators, Guests, Power Users, Replicator, Users +-- | Users: n/a +-- | Creation time: 2009-10-17 12:45:46 +-- | Passwords: min length: n/a; min age: n/a days; max age: 42 days; history: n/a passwords +-- |_ Account lockout disabled -- + ----------------------------------------------------------------------- author = "Ron Bowes" @@ -58,228 +60,58 @@ require 'msrpc' require 'smb' require 'stdnse' +require 'nsedebug' -- 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 = {} - local status, smbstate - local i, j + local status, result = msrpc.get_domains(host) - -- Create the SMB session - status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH) - if(status == false) then - return stdnse.format_output(false, smbstate) - end + if(not(status)) then + return stdnse.format_output(false, result) + else + local response = {} - -- Bind to SAMR service - status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil) - if(status == false) then - return stdnse.format_output(false, bind_result) - end + for domain, data in pairs(result) do + local piece = {} + piece['name'] = domain + + if(#data.groups > 0) then + table.insert(piece, string.format("Groups: %s", stdnse.strjoin(", ", data.groups))) + else + table.insert(piece, string.format("Groups: n/a")) + end - -- Call connect4() - status, connect4_result = msrpc.samr_connect4(smbstate, host.ip) - if(status == false) then - return stdnse.format_output(false, connect4_result) - end + if(#data.users > 0) then + table.insert(piece, string.format("Users: %s", stdnse.strjoin(", ", data.users))) + else + table.insert(piece, string.format("Users: n/a")) + end - -- Save the connect_handle - connect_handle = connect4_result['connect_handle'] + table.insert(piece, string.format("Creation time: %s", data.created)) + table.insert(piece, string.format("Passwords: min length: %s; min age: %s days; max age: %s days; history: %s passwords", + data.min_password_length or "n/a", + data.min_password_age or "n/a", + math.floor(data.max_password_age) or "n/a", + data.password_history or "n/a")) + if(data.password_properties and #data.password_properties) then + table.insert(piece, string.format("Properties: %s", stdnse.strjoin(", ", data.password_properties))) + end - -- Call EnumDomains() - status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle) - if(status == false) then - return stdnse.format_output(false, enumdomains_result) - end + if(data.lockout_threshold) then + table.insert(piece, string.format("Account lockout: %s attempts in %s minutes will lock out the account for %s minutes", data.lockout_threshold, data.lockout_window or "unlimited", data.lockout_duration or "unlimited")) + else + table.insert(piece, string.format("Account lockout disabled")) + end - -- If no domains were returned, print an error (I don't expect this will actually happen) - if(#enumdomains_result['sam']['entries'] == 0) then - 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 status, domain_info = get_domain_info(smbstate, domain) - - 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 - table.insert(response, domain_info) + table.insert(response, piece) end + return stdnse.format_output(true, response) end - - -- Close the connect handle - msrpc.samr_close(smbstate, connect_handle) - - -- Close the SMB session - msrpc.stop_smb(smbstate) - - return stdnse.format_output(true, response) end