Skip to content

Commit 31472fb

Browse files
authored
Merge branch 'main' into fix/mssql-integrated-auth-tds-error-handling
2 parents a1ecc1d + 1f4acea commit 31472fb

4 files changed

Lines changed: 66 additions & 34 deletions

File tree

nxc/modules/change-password.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ def on_login(self, context, connection):
122122
else:
123123
self.context.db.add_credential("plaintext", target_domain, target_username, self.newpass)
124124
except Exception as e:
125-
context.log.fail(f"SMB-SAMR password change failed: {e}")
125+
if "STATUS_ACCESS_DENIED" in str(e):
126+
self.context.log.fail(f"STATUS_ACCESS_DENIED while changing password for user: {target_username}")
127+
elif "STATUS_NONE_MAPPED" in str(e):
128+
self.context.log.fail(f"User '{target_username}' not found or not resolvable")
129+
else:
130+
context.log.fail(f"SMB-SAMR password change failed: {e}")
126131
finally:
127132
self.dce.disconnect()
128133

@@ -145,13 +150,9 @@ def _smb_samr_change(self, context, connection, target_username, target_domain,
145150
context.log.success(f"Successfully changed password for {target_username}")
146151

147152
def _hSamrOpenUser(self, connection, username):
148-
"""Get handle to the user object"""
149-
try:
150-
# Connect to the target server and retrieve handles
151-
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
152-
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
153-
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
154-
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
155-
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
156-
except Exception as e:
157-
self.context.log.fail(f"Failed to open user: {e}")
153+
"""Connect to the target server and retrieve the user handle"""
154+
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
155+
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
156+
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
157+
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
158+
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]

nxc/modules/pre2k.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,28 @@ class NXCModule:
1919
category = CATEGORY.PRIVILEGE_ESCALATION
2020

2121
def options(self, context, module_options):
22-
"""No options available"""
22+
"""
23+
ALL Attempt to authenticate for every computer object in the domain (default: False)
24+
25+
Examples:
26+
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k
27+
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k -o ALL=True
28+
"""
29+
self.all_option = module_options.get("ALL", "").lower() in ["true", "1", "yes"]
2330

2431
def on_login(self, context, connection):
25-
# Define the search filter for pre-created computer accounts
26-
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
32+
# Define the search filter
33+
if self.all_option:
34+
search_filter = "(&(objectClass=computer))"
35+
else:
36+
search_filter = "(&(objectClass=computer)(userAccountControl=4128))" # 4128 = 4096 (WORKSTATION_TRUST_ACCOUNT) | 32 (WORKSTATION_TRUST_ACCOUNT)
37+
2738
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
2839

2940
context.log.info(f"Using search filter: {search_filter}")
3041
context.log.info(f"Attributes to retrieve: {attributes}")
3142

32-
computers = []
43+
computers = {}
3344

3445
try:
3546
# Use paged search to retrieve all computer accounts with specific flags
@@ -39,27 +50,39 @@ def on_login(self, context, connection):
3950

4051
for computer in results:
4152
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
42-
# Check if the account is a pre-created computer account
43-
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
44-
computers.append(computer["sAMAccountName"])
45-
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
53+
computers[computer["sAMAccountName"]] = computer["userAccountControl"]
54+
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
4655

4756
# Save computers to file
4857
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
49-
output_file = os.path.join(domain_dir, "precreated_computers.txt")
58+
output_file_pre2k = os.path.join(domain_dir, "precreated_computers.txt")
59+
output_file_non_pre2k = os.path.join(domain_dir, "non_precreated_computers.txt")
5060

5161
# Create directories if they do not exist
5262
os.makedirs(domain_dir, exist_ok=True)
5363

54-
with open(output_file, "w") as file:
55-
for computer in computers:
56-
file.write(f"{computer}\n")
64+
with open(output_file_pre2k, "w") as pre2k_file, open(output_file_non_pre2k, "w") as non_pre2k_file:
65+
for computer, uac in computers.items():
66+
if int(uac) == 4128:
67+
pre2k_file.write(f"{computer}\n")
68+
else:
69+
non_pre2k_file.write(f"{computer}\n")
5770

58-
# Print discovered pre-created computer accounts
71+
# Print discovered (pre-created) computer accounts
5972
if computers:
60-
for computer in computers:
61-
context.log.highlight(f"Pre-created computer account: {computer}")
62-
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
73+
for computer, uac in computers.items():
74+
if int(uac) == 4128:
75+
context.log.highlight(f"Pre-created computer account: {computer}")
76+
else:
77+
context.log.debug(f"Computer account: {computer}")
78+
79+
counter_pre2k = len([v for v in computers.values() if int(v) == 4128])
80+
counter_non_pre2k = len([v for v in computers.values() if int(v) != 4128])
81+
82+
if counter_pre2k != 0:
83+
context.log.success(f"Found {counter_pre2k} pre-created computer accounts. Saved to {output_file_pre2k}")
84+
if counter_non_pre2k != 0:
85+
context.log.success(f"Found {counter_non_pre2k} normal computer accounts. Saved to {output_file_non_pre2k}")
6386
else:
6487
context.log.info("No pre-created computer accounts found.")
6588

@@ -76,7 +99,7 @@ def on_login(self, context, connection):
7699

77100
# Summary of TGT results
78101
if successful_tgts > 0:
79-
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
102+
context.log.success(f"Successfully obtained TGT for {successful_tgts} (pre-created) computer accounts. Saved to {ccache_base_dir}")
80103
except Exception as e:
81104
context.log.fail(f"Error occurred during search: {e}")
82105

@@ -101,7 +124,13 @@ def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
101124
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
102125
return True
103126
except Exception as e:
104-
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
127+
if "KDC_ERR_PREAUTH_FAILED" in str(e):
128+
if self.all_option:
129+
context.log.debug(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
130+
else:
131+
context.log.fail(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
132+
else:
133+
context.log.fail(f"Error obtaining TGT for {username}@{domain}: {e}")
105134
return False
106135

107136
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):

nxc/protocols/mssql/mssqlexec.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ def __init__(self, connection, logger):
1010
self.backuped_options = {}
1111

1212
def execute(self, command):
13-
result = None
13+
result = ""
1414

1515
self.backup_and_enable("advanced options")
1616
self.backup_and_enable("xp_cmdshell")
1717

1818
try:
1919
cmd = f"exec master..xp_cmdshell '{command}'"
2020
self.logger.debug(f"Attempting to execute query: {cmd}")
21-
result = self.mssql_conn.sql_query(cmd)
22-
self.logger.debug(f"Raw results from query: {result}")
23-
if result:
24-
result = "\n".join(line["output"] for line in result if line["output"] != "NULL")
21+
raw = self.mssql_conn.sql_query(cmd)
22+
self.logger.debug(f"Raw results from query: {raw}")
23+
if raw:
24+
result = "\n".join(line["output"] for line in raw if line["output"] != "NULL")
2525
self.logger.debug(f"Concatenated result together for easier parsing: {result}")
2626
# if you prepend SilentlyContinue it will still output the error, but it will still continue on (so it's not silent...)
2727
if "Preparing modules for first use" in result and "Completed" not in result:

tests/e2e_commands.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-net
229229
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME
230230
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps
231231
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq
232+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k
233+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k -o ALL=true
232234
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets
233235
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-desc
234236
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami

0 commit comments

Comments
 (0)