mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
Merged in significant changes to Microsoft RPC calls
This commit is contained in:
@@ -1,13 +1,34 @@
|
||||
description = [[
|
||||
Attempts to enumerate the users on a remote Windows system, with as much
|
||||
information as possible, through a variety of techniques (over SMB and MSRPC,
|
||||
which uses port 445 or 139). Some functions in SAMR are used to enumerate
|
||||
users, and some brute-force guessing using LSA functions is attempted.
|
||||
information as possible, through two different techniques (both over MSRPC,
|
||||
which uses port 445 or 139). Some SAMR functions are used to enumerate users,
|
||||
and bruteforce LSA guessing is attempted.
|
||||
|
||||
One technique used is calling the <code>QueryDisplayInfo</code> function in the SAMR library.
|
||||
If this succeeds, it will return a detailed list of users. This can be done
|
||||
anonymously against Windows 2000, and with a user-level account on other Windows
|
||||
versions (but not with a guest-level account).
|
||||
By default, both SAMR enumeration and LSA bruteforcing are used; however, these
|
||||
can be fine tuned using Nmap parameters. For the most possible information,
|
||||
leave the defaults; however, there are advantages to using them individually.
|
||||
|
||||
Advantages of using SAMR enumeration:
|
||||
* Stealthier (requires one packet/user account, whereas LSA uses at least 20
|
||||
packets; additionally, LSA makes a lot of noise in the Windows event log (LSA
|
||||
enumeration is the only script I (Ron Bowes) have been called on by the
|
||||
administrator of a box I was testing against).
|
||||
* More information is returned (more than just the username).
|
||||
* Every account will be found, since they're being enumerated with a function
|
||||
that's designed to enumerate users.
|
||||
|
||||
Advantages of using LSA bruteforcing:
|
||||
* More accounts are returned (system accounts, groups, and aliases are returned,
|
||||
not just users).
|
||||
* Requires a lower-level account to run on Windows XP and higher (a 'guest' account
|
||||
can be used, whereas SAMR enumeration requires a 'user' account; especially useful
|
||||
when only guest access is allowed, or when an account has a blank password (which
|
||||
effectively gives it guest access)).
|
||||
|
||||
SAMR enumeration is done with the <code>QueryDisplayInfo</code> function.
|
||||
If this succeeds, it will return a detailed list of users, along with descriptions,
|
||||
types, and full names. This can be done anonymously against Windows 2000, and
|
||||
with a user-level account on other Windows versions (but not with a guest-level account).
|
||||
|
||||
To perform this test, the following functions are used:
|
||||
* <code>Bind</code>: bind to the SAMR service.
|
||||
@@ -29,7 +50,7 @@ against Windows 2000, and requires a guest account or better on other systems.
|
||||
It has the advantage of running with less permission, and will also find more
|
||||
account types (i.e., groups, aliases, etc.). The disadvantages is that it returns
|
||||
less information, and that, because it's a brute-force guess, it's possible to miss
|
||||
accounts.
|
||||
accounts. It's also extremely noisy.
|
||||
|
||||
This isn't a brute-force technique in the common sense, however: it's a brute-forcing of users'
|
||||
RIDs. A user's RID is a value (generally 500, 501, or 1000+) that uniquely identifies
|
||||
@@ -37,26 +58,28 @@ a user on a domain or system. An LSA function is exposed which lets us convert t
|
||||
(say, 1000) to the username (say, "Ron"). So, the technique will essentially try
|
||||
converting 1000 to a name, then 1001, 1002, etc., until we think we're done.
|
||||
|
||||
Users are broken into groups of five RIDs, then checked individually (checking too many
|
||||
at once causes problems). We continue checking until we reach 1100, and get an empty
|
||||
group. This probably isn't the most effective way, but it seems to work.
|
||||
It might be a good idea to modify this, in the future, with some more
|
||||
To do this, this script breaks users into groups of five RIDs, then checked individually
|
||||
(checking too many at once causes problems). We continue checking until we reach
|
||||
1100, and get an empty group of five. This probably isn't the most effective way, but it
|
||||
seems to work. It might be a good idea to modify this, in the future, with some more
|
||||
intelligence. I (Ron Bowes) performed a test on an old server with a lot of accounts,
|
||||
and I got these results: 500, 501, 1000, 1030, 1031, 1053, 1054, 1055,
|
||||
and these were the active RIDs: 500, 501, 1000, 1030, 1031, 1053, 1054, 1055,
|
||||
1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1070,
|
||||
1075, 1081, 1088, 1090. The jump from 1000 to 1030 is quite large and can easily
|
||||
result in missing accounts, in an automated check.
|
||||
result in missing accounts, in an automated check. An ideal solution might be to continue
|
||||
doing groups of 5, but wait until we get 5-10 consecutive empty groups before giving up.
|
||||
|
||||
Before attempting this conversion, the SID of the server has to be determined.
|
||||
The SID is determined by doing the reverse operation, that is, converting a name into
|
||||
a RID. The name is determined by looking up any name present on the system.
|
||||
The SID is determined by doing the reverse operation; that is, by converting a name into
|
||||
its RID. The name is determined by looking up any name present on the system.
|
||||
We try:
|
||||
* The computer name and domain name, returned in <code>SMB_COM_NEGOTIATE</code>;
|
||||
* An nbstat query to get the server name and the user currently logged in; and
|
||||
* Some common names: "administrator", "guest", and "test".
|
||||
|
||||
In theory, the computer name should be sufficient for this to always work, and
|
||||
so far has in my tests, but I included the rest of the names for good measure.
|
||||
it has so far has in my tests, but I included the rest of the names for good measure. It
|
||||
doesn't hurt to add more.
|
||||
|
||||
The names and details from both of these techniques are merged and displayed.
|
||||
If the output is verbose, then extra details are shown. The output is ordered alphabetically.
|
||||
@@ -110,8 +133,14 @@ the code I wrote for this is largely based on the techniques used by them.
|
||||
-- |_ |_ Domain: LOCALSYSTEM
|
||||
--
|
||||
-- @args smb* This script supports the <code>smbusername</code>,
|
||||
-- <code>smbpassword</code>, <code>smbhash</code>, <code>smbguest</code>, and
|
||||
-- <code>smbtype</code> script arguments of the <code>smb</code> module.
|
||||
-- <code>smbpassword</code>, <code>smbhash</code>, and <code>smbtype</code>
|
||||
-- script arguments of the <code>smb</code> module.
|
||||
-- @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
|
||||
-- by using the default options.
|
||||
-- @args samronly If set, script will only query a list of users using a SAMR lookup. This is
|
||||
-- much quieter than LSA lookups, so enable this if you want stealth. Generally, however,
|
||||
-- you'll get better results by using the default options.
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
author = "Ron Bowes"
|
||||
@@ -136,6 +165,7 @@ end
|
||||
--<code>domain</code>, <code>fullname</code>, <code>rid</code>, and
|
||||
--<code>description</code>.
|
||||
local function enum_samr(host)
|
||||
local i, j
|
||||
|
||||
stdnse.print_debug(3, "Entering enum_samr()")
|
||||
|
||||
@@ -177,15 +207,15 @@ local function enum_samr(host)
|
||||
end
|
||||
|
||||
-- If no domains were returned, go back with an error
|
||||
if(#enumdomains_result['domains'] == 0) then
|
||||
if(#enumdomains_result['sam']['entries'] == 0) then
|
||||
msrpc.stop_smb(smbstate)
|
||||
return false, "Couldn't find any domains"
|
||||
end
|
||||
|
||||
-- Now, loop through the domains and find the users
|
||||
for i = 1, #enumdomains_result['domains'], 1 do
|
||||
for i = 1, #enumdomains_result['sam']['entries'], 1 do
|
||||
|
||||
local domain = enumdomains_result['domains'][i]
|
||||
local domain = enumdomains_result['sam']['entries'][i]['name']
|
||||
-- We don't care about the 'builtin' domain, in all my tests it's empty
|
||||
if(domain ~= 'Builtin') then
|
||||
local sid
|
||||
@@ -211,25 +241,53 @@ local function enum_samr(host)
|
||||
|
||||
-- Save the domain handle
|
||||
domain_handle = opendomain_result['domain_handle']
|
||||
|
||||
-- Call QueryDisplayInfo()
|
||||
status, querydisplayinfo_result = msrpc.samr_querydisplayinfo(smbstate, domain_handle)
|
||||
if(status == false) then
|
||||
msrpc.stop_smb(smbstate)
|
||||
return false, querydisplayinfo_result
|
||||
end
|
||||
|
||||
-- Loop as long as we're getting valid results
|
||||
j = 0
|
||||
repeat
|
||||
-- Call QueryDisplayInfo()
|
||||
status, querydisplayinfo_result = msrpc.samr_querydisplayinfo(smbstate, domain_handle, j)
|
||||
if(status == false) then
|
||||
msrpc.stop_smb(smbstate)
|
||||
return false, querydisplayinfo_result
|
||||
end
|
||||
|
||||
-- Save the response
|
||||
if(querydisplayinfo_result['return'] ~= 0 and querydisplayinfo_result['info'] ~= nil and querydisplayinfo_result['info']['entries'] ~= nil and querydisplayinfo_result['info']['entries'][1] ~= nil) then
|
||||
local array = {}
|
||||
local k
|
||||
|
||||
-- The reason these are all indexed from '1' is because we request names one at a time.
|
||||
array['domain'] = domain
|
||||
array['name'] = querydisplayinfo_result['info']['entries'][1]['account_name']
|
||||
array['fullname'] = querydisplayinfo_result['info']['entries'][1]['full_name']
|
||||
array['description'] = querydisplayinfo_result['info']['entries'][1]['description']
|
||||
array['rid'] = querydisplayinfo_result['info']['entries'][1]['rid']
|
||||
array['flags'] = querydisplayinfo_result['info']['entries'][1]['acct_flags']
|
||||
array['source'] = "SAMR Enumeration"
|
||||
|
||||
-- Clean up the 'flags' array
|
||||
for k = 1, #array['flags'], 1 do
|
||||
array['flags'][k] = msrpc.samr_AcctFlags_tostr(array['flags'][k])
|
||||
end
|
||||
|
||||
-- Add it to the array
|
||||
response[#response + 1] = array
|
||||
end
|
||||
j = j + 1
|
||||
until querydisplayinfo_result['return'] == 0
|
||||
|
||||
-- Close the domain handle
|
||||
msrpc.samr_close(smbstate, domain_handle)
|
||||
|
||||
-- Finally, fill in the response!
|
||||
for i = 1, #querydisplayinfo_result['details'], 1 do
|
||||
querydisplayinfo_result['details'][i]['domain'] = domain
|
||||
-- All we get from this is users
|
||||
querydisplayinfo_result['details'][i]['typestr'] = "User"
|
||||
querydisplayinfo_result['details'][i]['source'] = "SAMR Enumeration"
|
||||
response[#response + 1] = querydisplayinfo_result['details'][i]
|
||||
end
|
||||
-- for i = 1, #querydisplayinfo_result['details'], 1 do
|
||||
-- querydisplayinfo_result['details'][i]['domain'] = domain
|
||||
-- -- All we get from this is users
|
||||
-- querydisplayinfo_result['details'][i]['typestr'] = "User"
|
||||
-- querydisplayinfo_result['details'][i]['source'] = "SAMR Enumeration"
|
||||
-- response[#response + 1] = querydisplayinfo_result['details'][i]
|
||||
-- end
|
||||
end -- Checking for 'builtin'
|
||||
end -- Domain loop
|
||||
|
||||
@@ -301,31 +359,30 @@ local function enum_lsa(host)
|
||||
msrpc.stop_smb(smbstate)
|
||||
return false, lookupnames2_result
|
||||
end
|
||||
|
||||
-- Loop through the domains returned and find the users in each
|
||||
for i = 1, #lookupnames2_result['domains'], 1 do
|
||||
local domain = lookupnames2_result['domains'][i]['name']
|
||||
local sid = lookupnames2_result['domains'][i]['sid']
|
||||
local rids = { }
|
||||
for i = 1, #lookupnames2_result['domains']['domains'], 1 do
|
||||
local domain = lookupnames2_result['domains']['domains'][i]['name']
|
||||
local sid = lookupnames2_result['domains']['domains'][i]['sid']
|
||||
local sids = { }
|
||||
local start = 1000
|
||||
|
||||
-- Start by looking up 500 - 505 (will likely be Administrator + guest)
|
||||
for j = 500, 505, 1 do
|
||||
rids[#rids + 1] = j
|
||||
sids[#sids + 1] = sid .. "-" .. j
|
||||
end
|
||||
|
||||
status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sid, rids)
|
||||
status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sids)
|
||||
if(status == false) then
|
||||
stdnse.print_debug(1, string.format("Error looking up RIDs: %s", lookupsids2_result))
|
||||
else
|
||||
-- Put the details for each name into an array
|
||||
for j = 1, #lookupsids2_result['details'], 1 do
|
||||
if(lookupsids2_result['details'][j]['type'] ~= 8) then -- 8 = user not found
|
||||
-- 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
|
||||
local result = {}
|
||||
result['name'] = lookupsids2_result['details'][j]['name']
|
||||
result['rid'] = 500 + j - 1
|
||||
result['domain'] = domain
|
||||
result['typestr'] = lookupsids2_result['details'][j]['typestr']
|
||||
result['name'] = lookupsids2_result['names']['names'][j]['name']
|
||||
result['rid'] = 500 + j - 1
|
||||
result['domain'] = domain
|
||||
result['typestr'] = msrpc.lsa_SidType_tostr(lookupsids2_result['names']['names'][j]['sid_type'])
|
||||
result['source'] = "LSA Bruteforce"
|
||||
response[#response + 1] = result
|
||||
end
|
||||
@@ -335,29 +392,26 @@ local function enum_lsa(host)
|
||||
-- Now do groups of 5 users, until we get past 1100 and have an empty group
|
||||
repeat
|
||||
local used_names = 0
|
||||
local rids = {}
|
||||
local sids = {}
|
||||
for j = start, start + 4, 1 do
|
||||
rids[#rids + 1] = j
|
||||
sids[#sids + 1] = sid .. "-" .. j
|
||||
end
|
||||
|
||||
-- Try converting this group of RIDs into names
|
||||
status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sid, rids)
|
||||
status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sids)
|
||||
if(status == false) then
|
||||
stdnse.print_debug(1, string.format("Error looking up RIDs: %s", lookupsids2_result))
|
||||
else
|
||||
-- Put the details for each name into an array
|
||||
for j = 1, #lookupsids2_result['details'], 1 do
|
||||
if(lookupsids2_result['details'][j]['type'] ~= 8) then -- 8 = user not found
|
||||
for j = 1, #lookupsids2_result['names']['names'], 1 do
|
||||
if(lookupsids2_result['names']['names'][j]['sid_type'] ~= "SID_NAME_UNKNOWN") then
|
||||
local result = {}
|
||||
result['name'] = lookupsids2_result['details'][j]['name']
|
||||
result['rid'] = start + j - 1
|
||||
result['domain'] = domain
|
||||
result['typestr'] = lookupsids2_result['details'][j]['typestr']
|
||||
result['name'] = lookupsids2_result['names']['names'][j]['name']
|
||||
result['rid'] = start + j - 1
|
||||
result['domain'] = domain
|
||||
result['typestr'] = msrpc.lsa_SidType_tostr(lookupsids2_result['names']['names'][j]['sid_type'])
|
||||
result['source'] = "LSA Bruteforce"
|
||||
response[#response + 1] = result
|
||||
|
||||
-- Increment the number of used names we have
|
||||
used_names = used_names + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -381,38 +435,50 @@ end
|
||||
|
||||
action = function(host)
|
||||
local i, j
|
||||
local samr_status, lsa_status
|
||||
local samr_result, lsa_result
|
||||
local samr_status = false
|
||||
local lsa_status = false
|
||||
local samr_result = "Didn't run"
|
||||
local lsa_result = "Didn't run"
|
||||
local names = {}
|
||||
local name_strings = {}
|
||||
local response = " \n"
|
||||
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.
|
||||
lsa_status, lsa_result = enum_lsa(host)
|
||||
if(lsa_status == false) then
|
||||
if(nmap.debugging() > 0) then
|
||||
response = response .. "ERROR: couldn't enum 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
|
||||
names[string.upper(lsa_result[i]['name'])] = lsa_result[i]
|
||||
if(do_lsa) then
|
||||
lsa_status, lsa_result = enum_lsa(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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Try enumerating through SAMR
|
||||
samr_status, samr_result = enum_samr(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]
|
||||
if(do_samr) then
|
||||
samr_status, samr_result = enum_samr(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]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -434,7 +500,7 @@ action = function(host)
|
||||
|
||||
-- Check if we actually got any names back
|
||||
if(#name_strings == 0) then
|
||||
response = response .. "Sorry, couldn't find any account names anonymously!"
|
||||
response = response .. "Couldn't find any account names anonymously, sorry!"
|
||||
else
|
||||
-- If we're not verbose, just print out the names. Otherwise, print out everything we can
|
||||
if(nmap.verbosity() < 1) then
|
||||
@@ -457,7 +523,7 @@ action = function(host)
|
||||
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_list'])) end
|
||||
if(names[name]['flags'] ~= nil) then response = response .. string.format(" |_ Flags: %s\n", stdnse.strjoin(", ", names[name]['flags'])) end
|
||||
|
||||
if(nmap.verbosity() > 1) then
|
||||
if(names[name]['source'] ~= nil) then response = response .. string.format(" |_ Source: %s\n", names[name]['source']) end
|
||||
|
||||
Reference in New Issue
Block a user