Skip to content

Commit 0608628

Browse files
authored
Merge pull request #214 from Pennyw0rth/issue/203
Fix SMB users lookup and return last password set date
2 parents ae15861 + 020ec17 commit 0608628

File tree

3 files changed

+102
-48
lines changed

3 files changed

+102
-48
lines changed

nxc/protocols/smb.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,8 +1000,12 @@ def groups(self):
10001000
return groups
10011001

10021002
def users(self):
1003-
self.logger.display("Trying to dump local users with SAMRPC protocol")
1004-
return UserSamrDump(self).dump()
1003+
if len(self.args.users) > 1:
1004+
self.logger.display(f"Dumping users: {', '.join(self.args.users)}")
1005+
else:
1006+
self.logger.info("Trying to dump local users with SAMRPC protocol")
1007+
1008+
return UserSamrDump(self).dump(self.args.users)
10051009

10061010
def hosts(self):
10071011
hosts = []

nxc/protocols/smb/proto_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser):
3838
egroup.add_argument("--disks", action="store_true", help="enumerate disks")
3939
egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
4040
egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
41-
egroup.add_argument("--users", nargs="?", const="", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
41+
egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
4242
egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
4343
egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
4444
egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")

nxc/protocols/smb/samruser.py

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from impacket.dcerpc.v5.rpcrt import DCERPCException
55
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
66
from impacket.nt_errors import STATUS_MORE_ENTRIES
7+
from datetime import datetime, timedelta
78

89

910
class UserSamrDump:
@@ -26,6 +27,8 @@ def __init__(self, connection):
2627
self.doKerberos = connection.kerberos
2728
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
2829
self.users = []
30+
self.rpc_transport = None
31+
self.dce = None
2932

3033
if self.hash is not None:
3134
if self.hash.find(":") != -1:
@@ -36,46 +39,37 @@ def __init__(self, connection):
3639
if self.password is None:
3740
self.password = ""
3841

39-
def dump(self):
42+
def dump(self, requested_users=None):
4043
# Try all requested protocols until one works.
4144
for protocol in self.protocols:
4245
try:
4346
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
4447
port = protodef[1]
4548
except KeyError:
46-
self.logger.debug(f"Invalid Protocol '{protocol}'")
49+
self.logger.debug(f"Invalid Protocol: {protocol}")
50+
4751
self.logger.debug(f"Trying protocol {protocol}")
48-
rpctransport = transport.SMBTransport(
49-
self.addr,
50-
port,
51-
r"\samr",
52-
self.username,
53-
self.password,
54-
self.domain,
55-
self.lmhash,
56-
self.nthash,
57-
self.aesKey,
58-
doKerberos=self.doKerberos,
59-
)
52+
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos)
53+
6054
try:
61-
self.fetchList(rpctransport)
55+
self.fetch_users(requested_users)
6256
break
6357
except Exception as e:
64-
self.logger.debug(f"Protocol failed: {e}")
58+
self.logger.debug(f"Connection with protocol {protocol} failed: {e}")
6559
return self.users
6660

67-
def fetchList(self, rpctransport):
68-
dce = DCERPC_v5(rpctransport)
69-
dce.connect()
70-
dce.bind(samr.MSRPC_UUID_SAMR)
61+
def fetch_users(self, requested_users):
62+
self.dce = DCERPC_v5(self.rpc_transport)
63+
self.dce.connect()
64+
self.dce.bind(samr.MSRPC_UUID_SAMR)
7165

7266
# Setup Connection
73-
resp = samr.hSamrConnect2(dce)
67+
resp = samr.hSamrConnect2(self.dce)
7468
if resp["ErrorCode"] != 0:
7569
raise Exception("Connect error")
7670

7771
resp2 = samr.hSamrEnumerateDomainsInSamServer(
78-
dce,
72+
self.dce,
7973
serverHandle=resp["ServerHandle"],
8074
enumerationContext=0,
8175
preferedMaximumLength=500,
@@ -84,15 +78,15 @@ def fetchList(self, rpctransport):
8478
raise Exception("Connect error")
8579

8680
resp3 = samr.hSamrLookupDomainInSamServer(
87-
dce,
81+
self.dce,
8882
serverHandle=resp["ServerHandle"],
8983
name=resp2["Buffer"]["Buffer"][0]["Name"],
9084
)
9185
if resp3["ErrorCode"] != 0:
9286
raise Exception("Connect error")
9387

9488
resp4 = samr.hSamrOpenDomain(
95-
dce,
89+
self.dce,
9690
serverHandle=resp["ServerHandle"],
9791
desiredAccess=samr.MAXIMUM_ALLOWED,
9892
domainId=resp3["DomainId"],
@@ -101,28 +95,84 @@ def fetchList(self, rpctransport):
10195
raise Exception("Connect error")
10296

10397
self.__domains = resp2["Buffer"]["Buffer"]
104-
domainHandle = resp4["DomainHandle"]
98+
domain_handle = resp4["DomainHandle"]
10599
# End Setup
106100

107-
status = STATUS_MORE_ENTRIES
108-
enumerationContext = 0
109-
while status == STATUS_MORE_ENTRIES:
101+
if requested_users:
102+
self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}")
110103
try:
111-
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext)
104+
names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users)
105+
rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]]
106+
self.logger.debug(f"Specific RIDs retrieved: {rids}")
107+
users = self.get_user_info(domain_handle, rids)
112108
except DCERPCException as e:
113-
if str(e).find("STATUS_MORE_ENTRIES") < 0:
114-
self.logger.fail("Error enumerating domain user(s)")
115-
break
116-
resp = e.get_packet()
117-
self.logger.success("Enumerated domain user(s)")
118-
for user in resp["Buffer"]["Buffer"]:
119-
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
120-
info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"]
121-
self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}")
122-
self.users.append(user["Name"])
123-
samr.hSamrCloseHandle(dce, r["UserHandle"])
124-
125-
enumerationContext = resp["EnumerationContext"]
126-
status = resp["ErrorCode"]
127-
128-
dce.disconnect()
109+
self.logger.debug(f"Exception while requesting users in domain: {e}")
110+
if "STATUS_SOME_NOT_MAPPED" in str(e):
111+
# which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying
112+
self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again")
113+
else:
114+
self.logger.fail(f"Error occurred when looking up users in domain: {e}")
115+
else:
116+
status = STATUS_MORE_ENTRIES
117+
enumerationContext = 0
118+
while status == STATUS_MORE_ENTRIES:
119+
try:
120+
enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext)
121+
except DCERPCException as e:
122+
if str(e).find("STATUS_MORE_ENTRIES") < 0:
123+
self.logger.fail("Error enumerating domain user(s)")
124+
break
125+
enumerate_users_resp = e.get_packet()
126+
127+
rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]]
128+
self.logger.debug(f"Full domain RIDs retrieved: {rids}")
129+
users = self.get_user_info(domain_handle, rids)
130+
131+
# set these for the while loop
132+
enumerationContext = enumerate_users_resp["EnumerationContext"]
133+
status = enumerate_users_resp["ErrorCode"]
134+
self.print_user_info(users)
135+
self.dce.disconnect()
136+
137+
def get_user_info(self, domain_handle, user_ids):
138+
self.logger.debug(f"Getting user info for users: {user_ids}")
139+
users = []
140+
141+
for user in user_ids:
142+
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
143+
open_user_resp = samr.hSamrOpenUser(
144+
self.dce,
145+
domain_handle,
146+
samr.MAXIMUM_ALLOWED,
147+
user
148+
)
149+
info_user_resp = samr.hSamrQueryInformationUser2(
150+
self.dce,
151+
open_user_resp["UserHandle"],
152+
samr.USER_INFORMATION_CLASS.UserAllInformation
153+
)["Buffer"]
154+
155+
user_info = info_user_resp["All"]
156+
user_name = user_info["UserName"]
157+
bad_pwd_count = user_info["BadPasswordCount"]
158+
user_description = user_info["AdminComment"]
159+
last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"])
160+
if last_pw_set == "1601-01-01 00:00:00":
161+
last_pw_set = "<never>"
162+
users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set})
163+
164+
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
165+
return users
166+
167+
def print_user_info(self, users):
168+
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
169+
for user in users:
170+
self.logger.debug(f"Full user info: {user}")
171+
self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ")
172+
173+
174+
def old_large_int_to_datetime(large_int):
175+
combined = (large_int["HighPart"] << 32) | large_int["LowPart"]
176+
timestamp_seconds = combined / 10**7
177+
start_date = datetime(1601, 1, 1)
178+
return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")

0 commit comments

Comments
 (0)