1local msrpc = require "msrpc"
2local nmap = require "nmap"
3local smb = require "smb"
4local stdnse = require "stdnse"
5local string = require "string"
6local table = require "table"
7
8description = [[
9Attempts to enumerate the users on a remote Windows system, with as much
10information as possible, through two different techniques (both over MSRPC,
11which uses port 445 or 139; see <code>smb.lua</code>). The goal of this script
12is to discover all user accounts that exist on a remote system. This can be
13helpful for administration, by seeing who has an account on a server, or for
14penetration testing or network footprinting, by determining which accounts
15exist on a system.
16
17A penetration tester who is examining servers may wish to determine the
18purpose of a server. By getting a list of who has access to it, the tester
19might get a better idea (if financial people have accounts, it probably
20relates to financial information). Additionally, knowing which accounts
21exist on a system (or on multiple systems) allows the pen-tester to build a
22dictionary of possible usernames for bruteforces, such as a SMB bruteforce
23or a Telnet bruteforce. These accounts may be helpful for other purposes,
24such as using the accounts in Web applications on this or other servers.
25
26From a pen-testers perspective, retrieving the list of users on any
27given server creates endless possibilities.
28
29Users are enumerated in two different ways:  using SAMR enumeration or
30LSA bruteforcing. By default, both are used, but they have specific
31advantages and disadvantages. Using both is a great default, but in certain
32circumstances it may be best to give preference to one.
33
34Advantages of using SAMR enumeration:
35* 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).
36* More information is returned (more than just the username).
37* Every account will be found, since they're being enumerated with a function that's designed to enumerate users.
38
39Advantages of using LSA bruteforcing:
40* More accounts are returned (system accounts, groups, and aliases are returned, not just users).
41* 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)).
42
43SAMR enumeration is done with the  <code>QueryDisplayInfo</code> function.
44If this succeeds, it will return a detailed list of users, along with descriptions,
45types, and full names. This can be done anonymously against Windows 2000, and
46with a user-level account on other Windows versions (but not with a guest-level account).
47
48To perform this test, the following functions are used:
49* <code>Bind</code>: bind to the SAMR service.
50* <code>Connect4</code>: get a connect_handle.
51* <code>EnumDomains</code>: get a list of the domains.
52* <code>QueryDomain</code>: get the sid for the domain.
53* <code>OpenDomain</code>: get a handle for each domain.
54* <code>QueryDisplayInfo</code>: get the list of users in the domain.
55* <code>Close</code>: Close the domain handle.
56* <code>Close</code>: Close the connect handle.
57The advantage of this technique is that a lot of details are returned, including
58the full name and description; the disadvantage is that it requires a user-level
59account on every system except for Windows 2000. Additionally, it only pulls actual
60user accounts, not groups or aliases.
61
62Regardless of whether this succeeds, a second technique is used to pull
63user accounts, called LSA bruteforcing. LSA bruteforcing can be done anonymously
64against Windows 2000, and requires a guest account or better on other systems.
65It has the advantage of running with less permission, and will also find more
66account types (i.e., groups, aliases, etc.). The disadvantages is that it returns
67less information, and that, because it's a brute-force guess, it's possible to miss
68accounts. It's also extremely noisy.
69
70This isn't a brute-force technique in the common sense, however: it's a brute-forcing of users'
71RIDs. A user's RID is a value (generally 500, 501, or 1000+) that uniquely identifies
72a user on a domain or system. An LSA function is exposed which lets us convert the RID
73(say, 1000) to the username (say, "Ron"). So, the technique will essentially try
74converting 1000 to a name, then 1001, 1002, etc., until we think we're done.
75
76To do this, the script breaks users into groups of RIDs based on the <code>LSA_GROUPSIZE</code>
77constant. All members of this group are checked simultaneously, and the responses recorded.
78When a series of empty groups are found (<code>LSA_MINEMPTY</code> groups, specifically),
79the scan ends. As long as you are getting a few groups with active accounts, the scan will
80continue.
81
82Before attempting this conversion, the SID of the server has to be determined.
83The SID is determined by doing the reverse operation; that is, by converting a name into
84its RID. The name is determined by looking up any name present on the system.
85We try:
86* The computer name and domain name, returned in <code>SMB_COM_NEGOTIATE</code>;
87* An nbstat query to get the server name and the user currently logged in; and
88* Some common names: "administrator", "guest", and "test".
89
90In theory, the computer name should be sufficient for this to always work, and
91it has so far has in my tests, but I included the rest of the names for good measure. It
92doesn't hurt to add more.
93
94The names and details from both of these techniques are merged and displayed.
95If the output is verbose, then extra details are shown. The output is ordered alphabetically.
96
97Credit goes out to the <code>enum.exe</code>, <code>sid2user.exe</code>, and
98<code>user2sid.exe</code> programs for pioneering some of the techniques used
99in this script.
100]]
101
102---
103-- @usage
104-- nmap --script smb-enum-users.nse -p445 <host>
105-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139 <host>
106--
107-- @output
108-- Host script results:
109-- |  smb-enum-users:
110-- |_ |_ Domain: RON-WIN2K-TEST; Users: Administrator, Guest, IUSR_RON-WIN2K-TEST, IWAM_RON-WIN2K-TEST, test1234, TsInternetUser
111--
112-- Host script results:
113-- |  smb-enum-users:
114-- |  |  RON-WIN2K-TEST\Administrator (RID: 500)
115-- |  |  |  Description: Built-in account for administering the computer/domain
116-- |  |  |_ Flags:       Password does not expire, Normal user account
117-- |  |  RON-WIN2K-TEST\Guest (RID: 501)
118-- |  |  |  Description: Built-in account for guest access to the computer/domain
119-- |  |  |_ Flags:       Password not required, Password does not expire, Normal user account
120-- |  |  RON-WIN2K-TEST\IUSR_RON-WIN2K-TEST (RID: 1001)
121-- |  |  |  Full name:   Internet Guest Account
122-- |  |  |  Description: Built-in account for anonymous access to Internet Information Services
123-- |  |  |_ Flags:       Password not required, Password does not expire, Normal user account
124-- |  |  RON-WIN2K-TEST\IWAM_RON-WIN2K-TEST (RID: 1002)
125-- |  |  |  Full name:   Launch IIS Process Account
126-- |  |  |  Description: Built-in account for Internet Information Services to start out of process applications
127-- |  |  |_ Flags:       Password not required, Password does not expire, Normal user account
128-- |  |  RON-WIN2K-TEST\test1234 (RID: 1005)
129-- |  |  |_ Flags:       Normal user account
130-- |  |  RON-WIN2K-TEST\TsInternetUser (RID: 1000)
131-- |  |  |  Full name:   TsInternetUser
132-- |  |  |  Description: This user account is used by Terminal Services.
133-- |_ |_ |_ Flags:       Password not required, Password does not expire, Normal user account
134--
135-- @args lsaonly If set, script will only enumerate using an LSA bruteforce (requires less
136--       access than samr). Only set if you know what you're doing, you'll get better results
137--       by using the default options.
138-- @args samronly If set, script will only query a list of users using a SAMR lookup. This is
139--       much quieter than LSA lookups, so enable this if you want stealth. Generally, however,
140--       you'll get better results by using the default options.
141-----------------------------------------------------------------------
142
143author = "Ron Bowes"
144copyright = "Ron Bowes"
145license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
146categories = {"auth","intrusive"}
147dependencies = {"smb-brute"}
148
149
150hostrule = function(host)
151  return smb.get_port(host) ~= nil
152end
153
154action = function(host)
155
156  local i, j
157  local samr_status = false
158  local lsa_status  = false
159  local samr_result = "Didn't run"
160  local lsa_result  = "Didn't run"
161  local names = {}
162  local names_lookup = {}
163  local response = {}
164  local samronly = nmap.registry.args.samronly
165  local lsaonly  = nmap.registry.args.lsaonly
166  local do_samr  = samronly ~= nil or (samronly == nil and lsaonly == nil)
167  local do_lsa   = lsaonly  ~= nil or (samronly == nil and lsaonly == nil)
168
169  -- Try enumerating through SAMR. This is the better source of information, if we can get it.
170  if(do_samr) then
171    samr_status, samr_result = msrpc.samr_enum_users(host)
172
173    if(samr_status) then
174      -- Copy the returned array into the names[] table
175      stdnse.debug2("EnumUsers: Received %d names from SAMR", #samr_result)
176      for i = 1, #samr_result, 1 do
177        -- Insert the full info into the names list
178        table.insert(names, samr_result[i])
179        -- Set the names_lookup value to 'true' to avoid duplicates
180        names_lookup[samr_result[i]['name']] = true
181      end
182    end
183  end
184
185  -- Try enumerating through LSA.
186  if(do_lsa) then
187    lsa_status, lsa_result  = msrpc.lsa_enum_users(host)
188    if(lsa_status) then
189      -- Copy the returned array into the names[] table
190      stdnse.debug2("EnumUsers: Received %d names from LSA", #lsa_result)
191      for i = 1, #lsa_result, 1 do
192        if(lsa_result[i]['name'] ~= nil) then
193          -- Check if the name already exists
194          if(not(names_lookup[lsa_result[i]['name']])) then
195            table.insert(names, lsa_result[i])
196          end
197        end
198      end
199    end
200  end
201
202  -- Check if both failed
203  if(samr_status == false and lsa_status == false) then
204    if(string.find(lsa_result, 'ACCESS_DENIED')) then
205      return stdnse.format_output(false, "Access denied while trying to enumerate users; except against Windows 2000, Guest or better is typically required")
206    end
207
208    return stdnse.format_output(false, {"Couldn't enumerate users", "SAMR returned " .. samr_result, "LSA returned " .. lsa_result})
209  end
210
211  -- Sort them
212  table.sort(names, function (a, b) return string.lower(a.name) < string.lower(b.name) end)
213
214  -- Break them out by domain
215  local domains = {}
216  for _, name in ipairs(names) do
217    local domain    = name['domain']
218
219    -- Make sure the entry in the domains table exists
220    if(not(domains[domain])) then
221      domains[domain] = {}
222    end
223
224    table.insert(domains[domain], name)
225  end
226
227  -- Check if we actually got any names back
228  if(#names == 0) then
229    table.insert(response, "Couldn't find any account names, sorry!")
230  else
231    -- If we're not verbose, just print out the names. Otherwise, print out everything we can
232    if(nmap.verbosity() < 1) then
233      for domain, domain_users in pairs(domains) do
234        -- Make an impromptu list of users
235        local names = {}
236        for _, info in ipairs(domain_users) do
237          table.insert(names, info['name'])
238        end
239
240        -- Add this domain to the response
241        table.insert(response, string.format("Domain: %s; Users: %s", domain, table.concat(names, ", ")))
242      end
243    else
244      for domain, domain_users in pairs(domains) do
245        for _, info in ipairs(domain_users) do
246          local response_part = {}
247          response_part['name'] = string.format("%s\\%s (RID: %d)", domain, info['name'], info['rid'])
248
249          if(info['fullname']) then
250            table.insert(response_part, string.format("Full name:   %s", info['fullname']))
251          end
252          if(info['description']) then
253            table.insert(response_part, string.format("Description: %s", info['description']))
254          end
255          if(info['flags']) then
256            table.insert(response_part, string.format("Flags:       %s", table.concat(info['flags'], ", ")))
257          end
258
259          table.insert(response, response_part)
260        end
261      end
262    end
263  end
264
265  return stdnse.format_output(true, response)
266end
267
268