summaryrefslogtreecommitdiffstats
path: root/scripts/smb-enum-users.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/smb-enum-users.nse')
-rw-r--r--scripts/smb-enum-users.nse267
1 files changed, 267 insertions, 0 deletions
diff --git a/scripts/smb-enum-users.nse b/scripts/smb-enum-users.nse
new file mode 100644
index 0000000..19ba7fb
--- /dev/null
+++ b/scripts/smb-enum-users.nse
@@ -0,0 +1,267 @@
+local msrpc = require "msrpc"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to enumerate the users on a remote Windows system, with as much
+information as possible, through two different techniques (both over MSRPC,
+which uses port 445 or 139; see <code>smb.lua</code>). The goal of this script
+is to discover all user accounts that exist on a remote system. This can be
+helpful for administration, by seeing who has an account on a server, or for
+penetration testing or network footprinting, by determining which accounts
+exist on a system.
+
+A penetration tester who is examining servers may wish to determine the
+purpose of a server. By getting a list of who has access to it, the tester
+might get a better idea (if financial people have accounts, it probably
+relates to financial information). Additionally, knowing which accounts
+exist on a system (or on multiple systems) allows the pen-tester to build a
+dictionary of possible usernames for bruteforces, such as a SMB bruteforce
+or a Telnet bruteforce. These accounts may be helpful for other purposes,
+such as using the accounts in Web applications on this or other servers.
+
+From a pen-testers perspective, retrieving the list of users on any
+given server creates endless possibilities.
+
+Users are enumerated in two different ways: using SAMR enumeration or
+LSA bruteforcing. By default, both are used, but they have specific
+advantages and disadvantages. Using both is a great default, but in certain
+circumstances it may be best to give preference to one.
+
+Advantages of using SAMR enumeration:
+* Stealthier (requires one packet/user account, whereas LSA uses at least 10 packets while SAMR uses half that; 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.
+* <code>Connect4</code>: get a connect_handle.
+* <code>EnumDomains</code>: get a list of the domains.
+* <code>QueryDomain</code>: get the sid for the domain.
+* <code>OpenDomain</code>: get a handle for each domain.
+* <code>QueryDisplayInfo</code>: get the list of users in the domain.
+* <code>Close</code>: Close the domain handle.
+* <code>Close</code>: Close the connect handle.
+The advantage of this technique is that a lot of details are returned, including
+the full name and description; the disadvantage is that it requires a user-level
+account on every system except for Windows 2000. Additionally, it only pulls actual
+user accounts, not groups or aliases.
+
+Regardless of whether this succeeds, a second technique is used to pull
+user accounts, called LSA bruteforcing. LSA bruteforcing can be done anonymously
+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. 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
+a user on a domain or system. An LSA function is exposed which lets us convert the RID
+(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.
+
+To do this, the script breaks users into groups of RIDs based on the <code>LSA_GROUPSIZE</code>
+constant. All members of this group are checked simultaneously, and the responses recorded.
+When a series of empty groups are found (<code>LSA_MINEMPTY</code> groups, specifically),
+the scan ends. As long as you are getting a few groups with active accounts, the scan will
+continue.
+
+Before attempting this conversion, the SID of the server has to be determined.
+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
+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.
+
+Credit goes out to the <code>enum.exe</code>, <code>sid2user.exe</code>, and
+<code>user2sid.exe</code> programs for pioneering some of the techniques used
+in this script.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-users.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-enum-users:
+-- |_ |_ Domain: RON-WIN2K-TEST; Users: Administrator, Guest, IUSR_RON-WIN2K-TEST, IWAM_RON-WIN2K-TEST, test1234, TsInternetUser
+--
+-- Host script results:
+-- | smb-enum-users:
+-- | | 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
+-- 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"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+
+ local i, j
+ local samr_status = false
+ local lsa_status = false
+ local samr_result = "Didn't run"
+ local lsa_result = "Didn't run"
+ local names = {}
+ 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 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.debug2("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 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.debug2("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(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
+
+ -- Sort them
+ 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(#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
+ 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
+
+ -- Add this domain to the response
+ table.insert(response, string.format("Domain: %s; Users: %s", domain, table.concat(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(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", table.concat(info['flags'], ", ")))
+ end
+
+ table.insert(response, response_part)
+ end
+ end
+ end
+ end
+
+ return stdnse.format_output(true, response)
+end
+