description = [[
Attempts to enumerate domains on a system, along with their policies. This generally requires
credentials, except against Windows 2000. In addition to the actual domain, the "Builtin"
domain is generally displayed. Windows returns this in the list of domains, but its policies
don't appear to be used anywhere.
Much of the information provided is useful to a penetration tester, because it tells the
tester what types of policies to expect. For example, if passwords have a minimum length of 8,
the tester can trim his database to match; if the minimum length is 14, the tester will
probably start looking for sticky notes on people's monitors.
Another useful piece of information is the password lockouts -- a penetration tester often wants
to know whether or not there's a risk of negatively impacting a network, and this will
indicate it. The SID is displayed, which may be useful in other tools; the users are listed,
which uses different functions than smb-enum-users.nse
(though likely won't
get different results), and the date and time the domain was created may give some insight into
its history.
After the initial bind
to SAMR, the sequence of calls is:
* Connect4
: get a connect_handle
* EnumDomains
: get a list of the domains (stop here if you just want the names).
* QueryDomain
: get the SID for the domain.
* OpenDomain
: get a handle for each domain.
* QueryDomainInfo2
: get the domain information.
* QueryDomainUsers
: get a list of the users in the domain.
]]
---
--@usage
-- nmap --script smb-enum-domains.nse -p445
-- sudo nmap -sU -sS --script smb-enum-domains.nse -p U:137,T:139
--
--@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
--
-----------------------------------------------------------------------
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery","intrusive"}
dependencies = {"smb-brute"}
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 = {}
local status, smbstate
local i, j
-- Create the SMB session
status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH)
if(status == false) then
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
return stdnse.format_output(false, bind_result)
end
-- Call connect4()
status, connect4_result = msrpc.samr_connect4(smbstate, host.ip)
if(status == false) then
return stdnse.format_output(false, connect4_result)
end
-- Save the connect_handle
connect_handle = connect4_result['connect_handle']
-- Call EnumDomains()
status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle)
if(status == false) then
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
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)
end
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