Skip to content

Commit 12cb8e3

Browse files
authored
Merge branch 'Pennyw0rth:main' into pre2k-module-adjustment
2 parents 7fb80e6 + a073c2d commit 12cb8e3

5 files changed

Lines changed: 94 additions & 20 deletions

File tree

nxc/connection.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import contextlib
66

77
from os.path import isfile
8-
from threading import BoundedSemaphore
8+
from threading import BoundedSemaphore, Lock
99
from functools import wraps
1010
from time import sleep
1111
from ipaddress import ip_address
@@ -25,6 +25,7 @@
2525
from impacket.krb5.ccache import CCache
2626

2727
sem = BoundedSemaphore(1)
28+
fail_lock = Lock()
2829
global_failed_logins = 0
2930
user_failed_logins = {}
3031

@@ -315,26 +316,28 @@ def call_modules(self):
315316
def inc_failed_login(self, username):
316317
global global_failed_logins, user_failed_logins
317318

318-
if username not in user_failed_logins:
319-
user_failed_logins[username] = 0
319+
with fail_lock:
320+
if username not in user_failed_logins:
321+
user_failed_logins[username] = 0
320322

321-
user_failed_logins[username] += 1
322-
global_failed_logins += 1
323-
self.failed_logins += 1
323+
user_failed_logins[username] += 1
324+
global_failed_logins += 1
325+
self.failed_logins += 1
324326

325327
def over_fail_limit(self, username):
326328
global global_failed_logins, user_failed_logins
327329

328-
if global_failed_logins == self.args.gfail_limit:
329-
return True
330+
with fail_lock:
331+
if global_failed_logins == self.args.gfail_limit:
332+
return True
330333

331-
if self.failed_logins == self.args.fail_limit:
332-
return True
334+
if self.failed_logins == self.args.fail_limit:
335+
return True
333336

334-
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
335-
return True
337+
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
338+
return True
336339

337-
return False
340+
return False
338341

339342
def query_db_creds(self):
340343
"""Queries the database for credentials to be used for authentication.
@@ -481,8 +484,6 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
481484
- NTLM-hash (/kerberos)
482485
- AES-key
483486
"""
484-
if self.over_fail_limit(username):
485-
return False
486487
if self.args.continue_on_success and owned:
487488
return False
488489

@@ -498,6 +499,8 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
498499
sleep(value)
499500

500501
with sem:
502+
if self.over_fail_limit(username):
503+
return False
501504
if cred_type == "plaintext":
502505
if self.kerberos:
503506
self.logger.debug("Trying to authenticate using Kerberos")

nxc/modules/get-scriptpath.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import json
2+
from nxc.helpers.misc import CATEGORY
3+
from nxc.parsers.ldap_results import parse_result_attributes
4+
5+
6+
class NXCModule:
7+
"""
8+
Get the scriptPath attribute of users
9+
10+
Module by @wyndoo
11+
"""
12+
name = "get-scriptpath"
13+
description = "Get the scriptPath attribute of all users."
14+
supported_protocols = ["ldap"]
15+
category = CATEGORY.ENUMERATION
16+
17+
def options(self, context, module_options):
18+
"""
19+
FILTER Apply the FILTER (grep-like) (default: '')
20+
OUTPUTFILE Path to a file to save the results (default: None)
21+
"""
22+
self.filter = ""
23+
self.outputfile = None
24+
25+
if "FILTER" in module_options:
26+
self.filter = module_options["FILTER"]
27+
28+
if "OUTPUTFILE" in module_options:
29+
self.outputfile = module_options["OUTPUTFILE"]
30+
31+
def on_login(self, context, connection):
32+
# Building the search filter
33+
resp = connection.search(
34+
searchFilter="(scriptPath=*)",
35+
attributes=["sAMAccountName", "scriptPath"]
36+
)
37+
38+
context.log.debug(f"Total of records returned {len(resp)}")
39+
answers = parse_result_attributes(resp)
40+
context.log.debug(f"Filtering for scriptPath containing: {self.filter}")
41+
filtered_answers = list(filter(lambda x: self.filter in x["scriptPath"], answers))
42+
43+
if filtered_answers:
44+
context.log.success("Found the following attributes: ")
45+
for answer in filtered_answers:
46+
context.log.highlight(f"User: {answer['sAMAccountName']:<20} ScriptPath: {answer['scriptPath']}")
47+
48+
# Save the results to a file
49+
if self.outputfile:
50+
self.save_to_file(context, filtered_answers)
51+
else:
52+
context.log.fail("No results found after filtering.")
53+
54+
def save_to_file(self, context, answers):
55+
"""Save the results to a JSON file."""
56+
try:
57+
# Format answers as a list of dictionaries for JSON output
58+
json_data = [{"sAMAccountName": answer["sAMAccountName"], "scriptPath": answer["scriptPath"]} for answer in answers]
59+
60+
# Save the JSON data to the specified file
61+
with open(self.outputfile, "w") as f:
62+
json.dump(json_data, f, indent=4)
63+
context.log.success(f"Results successfully saved to {self.outputfile}")
64+
65+
except Exception as e:
66+
context.log.error(f"Failed to save results to file: {e}")

nxc/protocols/mssql.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ def handle_mssql_reply(self):
423423

424424
def rid_brute(self, max_rid=None):
425425
entries = []
426+
if self.conn.lastError:
427+
self.logger.fail(f"Cannot perform RID bruteforce due to invalid connection: {self.conn.lastError}")
428+
return entries
426429
if not max_rid:
427430
max_rid = int(self.args.rid_brute)
428431

@@ -435,6 +438,7 @@ def rid_brute(self, max_rid=None):
435438
domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4]
436439
except Exception as e:
437440
self.logger.fail(f"Error parsing SID. Not domain joined?: {e}")
441+
return entries
438442

439443
so_far = 0
440444
simultaneous = 1000

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-de
234234
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami
235235
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-computers
236236
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M raisechild
237+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-scriptpath
237238
##### WINRM
238239
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
239240
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig

0 commit comments

Comments
 (0)