Skip to content

Commit e0f5097

Browse files
committed
Resolve merge conflicts
2 parents 1261782 + 6c482cd commit e0f5097

18 files changed

Lines changed: 312 additions & 278 deletions

nxc/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
3838
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
3939
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
40-
40+
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
4141

4242
if len(host_info_colors) != 4:
4343
nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.")

nxc/data/nxc.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ audit_mode =
66
reveal_chars_of_pwd = 0
77
log_mode = False
88
host_info_colors = ["green", "red", "yellow", "cyan"]
9+
check_guest_account = False
910

1011
[BloodHound]
1112
bh_enabled = False

nxc/modules/badsuccessor.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,18 @@ def is_excluded_sid(self, sid, domain_sid):
9494
return True
9595
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
9696

97-
def get_domain_sid(self, ldap_session, base_dn):
97+
def get_domain_sid(self):
9898
"""Retrieve the domain SID from the domain object in LDAP"""
99-
r = ldap_session.search(
100-
searchBase=base_dn,
99+
r = self.connection.search(
101100
searchFilter="(objectClass=domain)",
102101
attributes=["objectSid"]
103102
)
104103
parsed = parse_result_attributes(r)
105104
if parsed and "objectSid" in parsed[0]:
106105
return parsed[0]["objectSid"]
107106

108-
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
109-
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()
110109
results = {}
111110
parsed = parse_result_attributes(entries)
112111
for entry in parsed:
@@ -146,24 +145,21 @@ def find_bad_successor_ous(self, ldap_session, entries, base_dn):
146145
results.setdefault(owner_sid, []).append(dn)
147146
return results
148147

149-
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
148+
def resolve_sid_to_name(self, sid):
150149
"""
151150
Resolves a SID to a samAccountName using LDAP
152151
153152
Args:
154153
----
155-
ldap_session: The LDAP connection
156154
sid: The SID to resolve
157-
base_dn: The base DN for the LDAP search
158155
159156
Returns:
160157
-------
161158
str: The samAccountName if found, otherwise the original SID
162159
"""
163160
try:
164161
search_filter = f"(objectSid={sid})"
165-
response = ldap_session.search(
166-
searchBase=base_dn,
162+
response = self.connection.search(
167163
searchFilter=search_filter,
168164
attributes=["sAMAccountName"]
169165
)
@@ -176,9 +172,10 @@ def resolve_sid_to_name(self, ldap_session, sid, base_dn):
176172
return sid
177173

178174
def on_login(self, context, connection):
175+
self.connection = connection
176+
179177
# Check for a domain controller with Windows Server 2025
180-
resp = connection.ldap_connection.search(
181-
searchBase=connection.ldap_connection._baseDN,
178+
resp = self.connection.search(
182179
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
183180
attributes=["operatingSystem", "dNSHostName"]
184181
)
@@ -194,27 +191,23 @@ def on_login(self, context, connection):
194191

195192
# Enumerate dMSA objects
196193
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
197-
resp = connection.ldap_connection.search(
198-
searchBase=connection.ldap_connection._baseDN,
194+
resp = self.connection.search(
199195
searchFilter="(objectClass=organizationalUnit)",
200196
attributes=["distinguishedName", "nTSecurityDescriptor"],
201-
searchControls=controls) # Fixed parameter name
197+
searchControls=controls
198+
)
202199

203200
context.log.debug(f"Found {len(resp)} entries")
204201

205-
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
202+
results = self.find_bad_successor_ous(resp)
206203

207204
if results:
208205
context.log.success(f"Found {len(results)} results")
209206
else:
210207
context.log.highlight("No account found")
211208

212209
for sid, ous in results.items():
213-
samaccountname = self.resolve_sid_to_name(
214-
connection.ldap_connection,
215-
sid,
216-
connection.ldap_connection._baseDN
217-
)
210+
samaccountname = self.resolve_sid_to_name(sid)
218211

219212
for ou in ous:
220213
if sid == samaccountname:

nxc/modules/coerce_plus.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from impacket import uuid
21
from impacket.dcerpc.v5 import transport, rprn, even, epm
32
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
43
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
54
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
6-
75
from impacket.uuid import uuidtup_to_bin
86
from nxc.helpers.misc import CATEGORY
7+
import contextlib
98

109

1110
class NXCModule:
@@ -213,6 +212,15 @@ def on_login(self, context, connection):
213212
context.log.error("Invalid method, please check the method name.")
214213
return
215214

215+
@staticmethod
216+
def get_dynamic_endpoint(interface: bytes, target: str, timeout: int = 5) -> str:
217+
string_binding = rf"ncacn_ip_tcp:{target}[135]"
218+
rpctransport = transport.DCERPCTransportFactory(string_binding)
219+
rpctransport.set_connect_timeout(timeout)
220+
dce = rpctransport.get_dce_rpc()
221+
dce.connect()
222+
return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
223+
216224

217225
class ShadowCoerceTrigger:
218226
def __init__(self, context):
@@ -530,6 +538,11 @@ def connect(self, username, password, domain, lmhash, nthash, aesKey, target, do
530538
},
531539
}
532540

541+
# activates EFS
542+
# https://specterops.io/blog/2025/08/19/will-webclient-start/
543+
with contextlib.suppress(Exception):
544+
NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("df1941c5-fe89-4e79-bf10-463657acf44d", "0.0")), target, timeout=1)
545+
533546
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
534547
rpctransport.set_dport(445)
535548

@@ -757,27 +770,6 @@ class PrinterBugTrigger:
757770
def __init__(self, context):
758771
self.context = context
759772

760-
def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str:
761-
string_binding = rf"ncacn_ip_tcp:{target}[135]"
762-
rpctransport = transport.DCERPCTransportFactory(string_binding)
763-
rpctransport.set_connect_timeout(timeout)
764-
dce = rpctransport.get_dce_rpc()
765-
self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
766-
try:
767-
dce.connect()
768-
except Exception as e:
769-
self.context.log.warning(f"Failed to connect to endpoint mapper: {e}")
770-
raise e
771-
try:
772-
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
773-
self.context.log.debug(
774-
f"Resolved dynamic endpoint {uuid.bin_to_string(interface)!r} to {endpoint!r}"
775-
)
776-
return endpoint
777-
except Exception as e:
778-
self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
779-
raise e
780-
781773
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
782774
binding_params = {
783775
"spoolss": {
@@ -786,7 +778,7 @@ def connect(self, username, password, domain, lmhash, nthash, aesKey, target, do
786778
"port": 445
787779
},
788780
"[dcerpc]": {
789-
"stringBinding": self.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target),
781+
"stringBinding": NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target),
790782
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
791783
"port": None
792784
}

nxc/modules/efsr_spray.py

Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,10 @@
1-
import ntpath
2-
from nxc.helpers.misc import CATEGORY, gen_random_string
31
from nxc.context import Context
4-
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_ATTRIBUTE_ENCRYPTED
5-
from impacket.smbconnection import SessionError, SMBConnection
6-
7-
8-
def get_error_string(exception):
9-
if hasattr(exception, "getErrorString"):
10-
try:
11-
es = exception.getErrorString()
12-
except KeyError:
13-
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
14-
if type(es) is tuple:
15-
return es[0]
16-
else:
17-
return es
18-
else:
19-
return str(exception)
2+
from nxc.helpers.misc import CATEGORY
203

214

225
class NXCModule:
23-
"""EFSR Spray Module
24-
Module by @rtpt-romankarwacik
25-
"""
26-
276
name = "efsr_spray"
28-
description = "Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
7+
description = "[REMOVED] Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
298
supported_protocols = ["smb"]
309
excluded_shares = ["SYSVOL"]
3110
category = CATEGORY.PRIVILEGE_ESCALATION
@@ -36,76 +15,6 @@ def options(self, context: Context, module_options: dict[str, str]):
3615
SHARE_NAME If set, ONLY this share will be used
3716
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
3817
"""
39-
self.file_name = module_options.get("FILE_NAME", ntpath.normpath("\\" + gen_random_string() + ".txt"))
40-
self.share_name = module_options.get("SHARE_NAME")
41-
if module_options.get("EXCLUDED_SHARES"):
42-
self.excluded_shares += module_options.get("EXCLUDED_SHARES", "").split(",")
4318

4419
def on_login(self, context: Context, connection):
45-
conn: SMBConnection = connection.conn # Because typing is broken due to smb being a folder and a file >:(
46-
47-
try:
48-
shares = conn.listShares()
49-
except SessionError as e:
50-
error = get_error_string(e)
51-
context.log.fail(f"Error enumerating shares: {error}", color="magenta")
52-
return
53-
54-
# Check if named pipe is already available
55-
try:
56-
named_pipe_names = [f.get_shortname() for f in conn.listPath("IPC$", "*")]
57-
if "efsrpc" in named_pipe_names:
58-
context.log.highlight("efsrpc named pipe is already available!")
59-
# if it is already activated we just skip this computer
60-
return
61-
except SessionError as e:
62-
error = get_error_string(e)
63-
context.log.fail(f"Error enumerating named pipes: {error}", color="magenta")
64-
return
65-
66-
# Write an encrypted file on the share root.
67-
# This will likely fail with STATUS_ACCESS_DENIED if we do not have the permission to create encrypted files,
68-
# but this does not matter as the service will be activated nevertheless if we have WRITE or MODIFY access
69-
for share in shares:
70-
share_name = share["shi1_netname"][:-1]
71-
if self.share_name is not None and self.share_name != share_name:
72-
continue
73-
74-
if share_name in self.excluded_shares:
75-
continue
76-
77-
try:
78-
context.log.debug(f"Connecting to share {share_name}...")
79-
tid = conn.connectTree(share_name)
80-
except SessionError as e:
81-
context.log.debug(f"Could not connect to share {share_name}: {e}")
82-
continue
83-
try:
84-
context.log.debug(f"Creating file in {share_name}...")
85-
fid = conn.createFile(tid, self.file_name,
86-
desiredAccess=FILE_SHARE_WRITE,
87-
shareMode=FILE_SHARE_DELETE,
88-
fileAttributes=FILE_ATTRIBUTE_ENCRYPTED)
89-
conn.closeFile(tid, fid)
90-
try:
91-
# this can happen when we have special permissions to create encrypted files
92-
conn.deleteFile(share_name, self.file_name)
93-
except SessionError as e:
94-
error = get_error_string(e)
95-
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
96-
pass
97-
context.log.fail(f"Error DELETING created temp file {self.file_name} on share {share_name}: {error}")
98-
except SessionError as e:
99-
context.log.debug(f"Error writing encrypted file on share {share_name}: {get_error_string(e)} (This does not necessarily mean that the attack failed!)")
100-
101-
try:
102-
tid = conn.connectTree("IPC$")
103-
conn.waitNamedPipe(tid, "efsrpc", 10)
104-
context.log.highlight("Successfully activated efsrpc named pipe!")
105-
except SessionError as e:
106-
error = get_error_string(e)
107-
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
108-
context.log.debug("efsrpc pipe was not activated.")
109-
else:
110-
context.log.fail(f"Error waiting for named pipe: {error}", color="magenta")
111-
return
20+
context.log.fail('[REMOVED] This module has been made obsolete and EFS will be activated automatically by "coerce_plus"')

nxc/modules/example_module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def options(self, context, module_options):
2121
"""Required.
2222
Module options get parsed here. Additionally, put the modules usage here as well
2323
"""
24+
# Put "No options available" in the docstring if there are no options for the module
2425

2526
def on_login(self, context, connection):
2627
"""Concurrent.

nxc/modules/gpp_password.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ class NXCModule:
1818
category = CATEGORY.CREDENTIAL_DUMPING
1919

2020
def options(self, context, module_options):
21-
""" """
21+
"""No options available"""
2222

2323
def on_login(self, context, connection):
2424
shares = connection.shares()
2525
for share in shares:
26-
if share["name"] == "SYSVOL" and "READ" in share["access"]:
26+
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
27+
sysvol = share["name"]
2728
context.log.success("Found SYSVOL share")
2829
context.log.display("Searching for potential XML files containing passwords")
2930

3031
paths = connection.spider(
31-
"SYSVOL",
32+
sysvol,
3233
pattern=[
3334
"Groups.xml",
3435
"Services.xml",
@@ -43,7 +44,7 @@ def on_login(self, context, connection):
4344
context.log.display(f"Found {path}")
4445

4546
buf = BytesIO()
46-
connection.conn.getFile("SYSVOL", path, buf.write)
47+
connection.conn.getFile(sysvol, path, buf.write)
4748
xml = ET.fromstring(buf.getvalue())
4849
sections = []
4950

0 commit comments

Comments
 (0)