Skip to content

Commit ae8fc46

Browse files
committed
Merge branch 'main' into passpool
2 parents 5789296 + 6e7edbd commit ae8fc46

132 files changed

Lines changed: 516 additions & 310 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/misc.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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
810
from time import strftime, gmtime
911

1012

@@ -222,3 +224,21 @@ def convert(low, high, lockout=False):
222224
elif minutes == 1:
223225
time += f"{minutes} minute "
224226
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def module_is_sane(self, module, module_path):
3030
elif not hasattr(module, "description"):
3131
self.logger.fail(f"{module_path} missing the description variable")
3232
module_error = True
33+
elif not hasattr(module, "category"):
34+
self.logger.fail(f"{module_path} missing the category variable")
35+
module_error = True
3336
elif not hasattr(module, "supported_protocols"):
3437
self.logger.fail(f"{module_path} missing the supported_protocols variable")
3538
module_error = True
@@ -92,6 +95,7 @@ def get_module_info(self, module_path):
9295
"description": module_spec.description,
9396
"options": module_spec.options.__doc__,
9497
"supported_protocols": module_spec.supported_protocols,
98+
"category": module_spec.category,
9599
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
96100
}
97101
}

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
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

nxc/modules/badsuccessor.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from impacket.ldap import ldaptypes
2+
from nxc.helpers.misc import CATEGORY
23
from nxc.parsers.ldap_results import parse_result_attributes
34
from ldap3.protocol.microsoft import security_descriptor_control
45

@@ -79,6 +80,7 @@ class NXCModule:
7980
name = "badsuccessor"
8081
description = "Check if vulnerable to bad successor attack (DMSA)"
8182
supported_protocols = ["ldap"]
83+
category = CATEGORY.ENUMERATION
8284

8385
def __init__(self):
8486
self.context = None
@@ -92,19 +94,18 @@ def is_excluded_sid(self, sid, domain_sid):
9294
return True
9395
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
9496

95-
def get_domain_sid(self, ldap_session, base_dn):
97+
def get_domain_sid(self):
9698
"""Retrieve the domain SID from the domain object in LDAP"""
97-
r = ldap_session.search(
98-
searchBase=base_dn,
99+
r = self.connection.search(
99100
searchFilter="(objectClass=domain)",
100101
attributes=["objectSid"]
101102
)
102103
parsed = parse_result_attributes(r)
103104
if parsed and "objectSid" in parsed[0]:
104105
return parsed[0]["objectSid"]
105106

106-
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
107-
domain_sid = self.get_domain_sid(ldap_session, base_dn)
107+
def find_bad_successor_ous(self, entries):
108+
domain_sid = self.get_domain_sid()
108109
results = {}
109110
parsed = parse_result_attributes(entries)
110111
for entry in parsed:
@@ -144,24 +145,21 @@ def find_bad_successor_ous(self, ldap_session, entries, base_dn):
144145
results.setdefault(owner_sid, []).append(dn)
145146
return results
146147

147-
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
148+
def resolve_sid_to_name(self, sid):
148149
"""
149150
Resolves a SID to a samAccountName using LDAP
150151
151152
Args:
152153
----
153-
ldap_session: The LDAP connection
154154
sid: The SID to resolve
155-
base_dn: The base DN for the LDAP search
156155
157156
Returns:
158157
-------
159158
str: The samAccountName if found, otherwise the original SID
160159
"""
161160
try:
162161
search_filter = f"(objectSid={sid})"
163-
response = ldap_session.search(
164-
searchBase=base_dn,
162+
response = self.connection.search(
165163
searchFilter=search_filter,
166164
attributes=["sAMAccountName"]
167165
)
@@ -174,9 +172,10 @@ def resolve_sid_to_name(self, ldap_session, sid, base_dn):
174172
return sid
175173

176174
def on_login(self, context, connection):
175+
self.connection = connection
176+
177177
# Check for a domain controller with Windows Server 2025
178-
resp = connection.ldap_connection.search(
179-
searchBase=connection.ldap_connection._baseDN,
178+
resp = self.connection.search(
180179
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
181180
attributes=["operatingSystem", "dNSHostName"]
182181
)
@@ -192,27 +191,23 @@ def on_login(self, context, connection):
192191

193192
# Enumerate dMSA objects
194193
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
195-
resp = connection.ldap_connection.search(
196-
searchBase=connection.ldap_connection._baseDN,
194+
resp = self.connection.search(
197195
searchFilter="(objectClass=organizationalUnit)",
198196
attributes=["distinguishedName", "nTSecurityDescriptor"],
199-
searchControls=controls) # Fixed parameter name
197+
searchControls=controls
198+
)
200199

201200
context.log.debug(f"Found {len(resp)} entries")
202201

203-
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
202+
results = self.find_bad_successor_ous(resp)
204203

205204
if results:
206205
context.log.success(f"Found {len(results)} results")
207206
else:
208207
context.log.highlight("No account found")
209208

210209
for sid, ous in results.items():
211-
samaccountname = self.resolve_sid_to_name(
212-
connection.ldap_connection,
213-
sid,
214-
connection.ldap_connection._baseDN
215-
)
210+
samaccountname = self.resolve_sid_to_name(sid)
216211

217212
for ou in ous:
218213
if sid == samaccountname:

nxc/modules/bitlocker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from impacket.dcerpc.v5.dtypes import NULL
44
from impacket.dcerpc.v5.dcomrt import DCOMConnection
55
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
6+
from nxc.helpers.misc import CATEGORY
67

78

89
class NXCModule:
910
name = "bitlocker"
1011
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
1112
supported_protocols = ["smb", "wmi"]
13+
category = CATEGORY.ENUMERATION
1214

1315
def __init__(self, context=None, module_options=None):
1416
self.context = context

nxc/modules/change-password.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
from impacket.dcerpc.v5 import samr, epm, transport
33
from impacket.dcerpc.v5.rpcrt import DCERPCException
4+
from nxc.helpers.misc import CATEGORY
45

56

67
class NXCModule:
@@ -12,6 +13,7 @@ class NXCModule:
1213
name = "change-password"
1314
description = "Change or reset user passwords via various protocols"
1415
supported_protocols = ["smb"]
16+
category = CATEGORY.PRIVILEGE_ESCALATION
1517

1618
def options(self, context, module_options):
1719
"""

0 commit comments

Comments
 (0)