Skip to content

Commit 49b5a60

Browse files
committed
resolve merge conflicts and sync with upstream
2 parents bdc4f73 + 027e520 commit 49b5a60

138 files changed

Lines changed: 950 additions & 519 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

nxc/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ def gen_cli_args():
7676
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
7777
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
7878
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
79-
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
79+
mgroup.add_argument("-L", "--list-modules", nargs="?", type=str, const="", help="list available modules")
8080
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
8181

8282
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
8383

8484
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
85-
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
85+
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules is not None or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
8686
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
8787
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
8888
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")

nxc/helpers/args.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,18 @@ def __call__(self, parser, namespace, values, option_string=None):
2424
# Set an attribute to track whether the value was explicitly set
2525
setattr(namespace, self.dest, values)
2626
setattr(namespace, f"{self.dest}_explicitly_set", True)
27+
28+
29+
def get_conditional_action(baseAction):
30+
class ConditionalAction(baseAction):
31+
def __init__(self, option_strings, dest, **kwargs):
32+
x = kwargs.pop("make_required", [])
33+
super().__init__(option_strings, dest, **kwargs)
34+
self.make_required = x
35+
36+
def __call__(self, parser, namespace, values, option_string=None):
37+
for x in self.make_required:
38+
x.required = True
39+
super().__call__(parser, namespace, values, option_string)
40+
41+
return ConditionalAction

nxc/helpers/misc.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from enum import Enum
12
import random
23
import string
34
import re
45
import inspect
56
import os
6-
7+
from termcolor import colored
78
from ipaddress import ip_address
9+
from nxc.logger import nxc_logger
10+
from time import strftime, gmtime
811

912

1013
def identify_target_file(target_file):
@@ -145,3 +148,97 @@ def detect_if_ip(target):
145148
return True
146149
except Exception:
147150
return False
151+
152+
153+
def d2b(a):
154+
"""
155+
Function used to convert password property flags from decimal to binary
156+
format for easier interpretation of individual flag bits.
157+
"""
158+
tbin = []
159+
while a:
160+
tbin.append(a % 2)
161+
a //= 2
162+
163+
t2bin = tbin[::-1]
164+
if len(t2bin) != 8:
165+
for _x in range(6 - len(t2bin)):
166+
t2bin.insert(0, 0)
167+
return "".join([str(g) for g in t2bin])
168+
169+
170+
def convert(low, high, lockout=False):
171+
"""
172+
Convert Windows FILETIME (64-bit) values to human-readable time strings.
173+
174+
Windows stores time intervals as 64-bit values representing 100-nanosecond
175+
intervals since January 1, 1601. This function converts these values to
176+
readable format like "30 days 5 hours 15 minutes".
177+
178+
Args:
179+
low (int): Low 32 bits of the FILETIME value
180+
high (int): High 32 bits of the FILETIME value
181+
lockout (bool): If True, treats the value as a lockout duration (simpler conversion)
182+
183+
Returns:
184+
str: Human-readable time string (e.g., "42 days 5 hours 30 minutes") or
185+
special values like "Not Set", "None", or "[-] Invalid TIME"
186+
"""
187+
time = ""
188+
tmp = 0
189+
190+
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
191+
return "Not Set"
192+
if low == 0 and high == 0:
193+
return "None"
194+
195+
if not lockout:
196+
if low != 0:
197+
high = abs(high + 1)
198+
else:
199+
high = abs(high)
200+
low = abs(low)
201+
202+
tmp = low + (high << 32) # convert to 64bit int
203+
tmp *= 1e-7 # convert to seconds
204+
else:
205+
tmp = abs(high) * (1e-7)
206+
207+
try:
208+
minutes = int(strftime("%M", gmtime(tmp)))
209+
hours = int(strftime("%H", gmtime(tmp)))
210+
days = int(strftime("%j", gmtime(tmp))) - 1
211+
except ValueError:
212+
return "[-] Invalid TIME"
213+
214+
if days > 1:
215+
time += f"{days} days "
216+
elif days == 1:
217+
time += f"{days} day "
218+
if hours > 1:
219+
time += f"{hours} hours "
220+
elif hours == 1:
221+
time += f"{hours} hour "
222+
if minutes > 1:
223+
time += f"{minutes} minutes "
224+
elif minutes == 1:
225+
time += f"{minutes} minute "
226+
return time
227+
228+
229+
def display_modules(args, modules):
230+
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
231+
# Add category filter for module listing
232+
if args.list_modules and args.list_modules.lower() != category.name.lower():
233+
continue
234+
if len([module for module in modules.values() if module["category"] == category]) > 0:
235+
nxc_logger.highlight(colored(f"{category.name}", color, attrs=["bold"]))
236+
for name, props in sorted(modules.items()):
237+
if props["category"] == category:
238+
nxc_logger.display(f"{name:<25} {props['description']}")
239+
240+
241+
class CATEGORY(Enum):
242+
ENUMERATION = "Enumeration"
243+
CREDENTIAL_DUMPING = "Credential Dumping"
244+
PRIVILEGE_ESCALATION = "Privilege Escalation"

nxc/loaders/moduleloader.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from os.path import join as path_join
99

1010
from nxc.context import Context
11+
from nxc.helpers.misc import CATEGORY
1112
from nxc.logger import NXCAdapter
1213
from nxc.paths import NXC_PATH
1314

@@ -30,6 +31,9 @@ def module_is_sane(self, module, module_path):
3031
elif not hasattr(module, "description"):
3132
self.logger.fail(f"{module_path} missing the description variable")
3233
module_error = True
34+
elif not hasattr(module, "category") or module.category not in [CATEGORY.ENUMERATION, CATEGORY.CREDENTIAL_DUMPING, CATEGORY.PRIVILEGE_ESCALATION]:
35+
self.logger.fail(f"{module_path} missing the category variable or invalid category")
36+
module_error = True
3337
elif not hasattr(module, "supported_protocols"):
3438
self.logger.fail(f"{module_path} missing the supported_protocols variable")
3539
module_error = True
@@ -92,6 +96,7 @@ def get_module_info(self, module_path):
9296
"description": module_spec.description,
9397
"options": module_spec.options.__doc__,
9498
"supported_protocols": module_spec.supported_protocols,
99+
"category": module_spec.category,
95100
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
96101
}
97102
}

nxc/modules/adcs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from impacket.ldap import ldap, ldapasn1
33
from impacket.ldap.ldap import LDAPSearchError
4+
from nxc.helpers.misc import CATEGORY
45

56

67
class NXCModule:
@@ -13,6 +14,7 @@ class NXCModule:
1314
name = "adcs"
1415
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
1516
supported_protocols = ["ldap"]
17+
category = CATEGORY.ENUMERATION
1618

1719
def __init__(self, context=None, module_options=None):
1820
self.context = context

nxc/modules/add-computer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
from impacket.dcerpc.v5 import samr, epm, transport
55
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
6+
from nxc.helpers.misc import CATEGORY
67

78

89
class NXCModule:
@@ -16,6 +17,7 @@ class NXCModule:
1617
name = "add-computer"
1718
description = "Adds or deletes a domain computer"
1819
supported_protocols = ["smb"]
20+
category = CATEGORY.PRIVILEGE_ESCALATION
1921

2022
def options(self, context, module_options):
2123
"""

nxc/modules/aws-credentials.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from nxc.helpers.misc import CATEGORY
2+
3+
14
class NXCModule:
25
"""
36
Search for aws credentials files on linux and windows machines
@@ -8,6 +11,7 @@ class NXCModule:
811
name = "aws-credentials"
912
description = "Search for aws credentials files."
1013
supported_protocols = ["ssh", "smb", "winrm"]
14+
category = CATEGORY.CREDENTIAL_DUMPING
1115

1216
def __init__(self):
1317
self.search_path_linux = "'/home/' '/tmp/'"

nxc/modules/backup_operator.py

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import time
1+
import contextlib
22
import os
33
import datetime
44

55
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
66
from impacket.smbconnection import SessionError
77
from impacket.dcerpc.v5 import transport, rrp
88
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
9+
from nxc.helpers.misc import CATEGORY
910
from nxc.paths import NXC_PATH
1011

1112

1213
class NXCModule:
1314
name = "backup_operator"
1415
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
1516
supported_protocols = ["smb"]
17+
category = CATEGORY.PRIVILEGE_ESCALATION
1618

1719
def __init__(self, context=None, module_options=None):
1820
self.context = context
@@ -28,18 +30,19 @@ def on_login(self, context, connection):
2830
connection.args.share = "SYSVOL"
2931
# enable remote registry
3032
context.log.display("Triggering RemoteRegistry to start through named pipe...")
31-
self.trigger_winreg(connection.conn, context)
33+
connection.trigger_winreg()
3234
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
3335
rpc.set_smb_connection(connection.conn)
3436
if connection.kerberos:
3537
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
3638
dce = rpc.get_dce_rpc()
3739
if connection.kerberos:
3840
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
39-
dce.connect()
40-
dce.bind(rrp.MSRPC_UUID_RRP)
4141

4242
try:
43+
dce.connect()
44+
dce.bind(rrp.MSRPC_UUID_RRP)
45+
4346
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
4447
hRootKey, subKey = self._strip_root_key(dce, hive)
4548
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
@@ -52,9 +55,11 @@ def on_login(self, context, connection):
5255
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
5356
return
5457
except (Exception, KeyboardInterrupt) as e:
55-
context.log.fail(str(e))
58+
context.log.fail(f"Unexpected error: {e}")
59+
return
5660
finally:
57-
dce.disconnect()
61+
with contextlib.suppress(Exception):
62+
dce.disconnect()
5863

5964
# copy remote file to local
6065
log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-"))
@@ -113,29 +118,3 @@ def parse_sam(secret):
113118
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
114119
else:
115120
context.log.display("Successfully deleted dump files !")
116-
117-
def trigger_winreg(self, connection, context):
118-
# Original idea from https://twitter.com/splinter_code/status/1715876413474025704
119-
# Basically triggers the RemoteRegistry to start without admin privs
120-
tid = connection.connectTree("IPC$")
121-
try:
122-
connection.openFile(
123-
tid,
124-
r"\winreg",
125-
0x12019F,
126-
creationOption=0x40,
127-
fileAttributes=0x80,
128-
)
129-
except SessionError as e:
130-
# STATUS_PIPE_NOT_AVAILABLE error is expected
131-
context.log.debug(str(e))
132-
# Give remote registry time to start
133-
time.sleep(1)
134-
135-
def _strip_root_key(self, dce, key_name):
136-
# Let's strip the root key
137-
key_name.split("\\")[0]
138-
sub_key = "\\".join(key_name.split("\\")[1:])
139-
ans = rrp.hOpenLocalMachine(dce)
140-
h_root_key = ans["phKey"]
141-
return h_root_key, sub_key

0 commit comments

Comments
 (0)