Skip to content

Commit 71cb5e9

Browse files
authored
Merge pull request Pennyw0rth#361 from Pennyw0rth/dcom_fix
Fix mmcexec method thanks to @IppSec AND a lot of other small things
2 parents ccbeb4e + d036fa8 commit 71cb5e9

10 files changed

Lines changed: 92 additions & 77 deletions

File tree

nxc/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
8585
def requires_admin(func):
8686
def _decorator(self, *args, **kwargs):
8787
if self.admin_privs is False:
88+
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
89+
return func(self, *args, **kwargs)
8890
return None
8991
return func(self, *args, **kwargs)
9092

@@ -146,6 +148,7 @@ def __init__(self, args, db, target):
146148
self.kdcHost = self.args.kdcHost
147149
self.port = self.args.port
148150
self.local_ip = None
151+
self.dns_server = self.args.dns_server
149152

150153
# DNS resolution
151154
dns_result = self.resolver(target)
@@ -541,7 +544,7 @@ def login(self):
541544

542545
if hasattr(self.args, "laps") and self.args.laps:
543546
self.logger.debug("Trying to authenticate using LAPS")
544-
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
547+
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
545548
cred_type = ["plaintext"]
546549
if not (username[0] or secret[0] or domain[0]):
547550
return False

nxc/modules/add-computer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ def do_samr_add(self, context):
102102
None
103103
"""
104104
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
105+
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
105106

106-
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
107+
rpc_transport = transport.DCERPCTransportFactory(string_binding)
107108
rpc_transport.setRemoteHost(self.__host)
108109

109110
if hasattr(rpc_transport, "set_credentials"):

nxc/modules/laps.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ def on_login(self, context, connection):
4545
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
4646
if "mslaps-encryptedpassword" in values:
4747
msMCSAdmPwd = values["mslaps-encryptedpassword"]
48-
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
48+
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
4949
try:
5050
data = d.run()
5151
except Exception as e:
52-
self.logger.fail(str(e))
53-
return
52+
context.log.fail(str(e))
53+
continue
5454
r = json.loads(data)
5555
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
5656
elif "mslaps-password" in values:

nxc/protocols/ldap.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ def get_ldap_info(self, host):
172172
self.logger.debug(f"ldap_connection: {ldap_connection}")
173173
except SysCallError as e:
174174
if proto == "ldaps":
175-
self.logger.debug(f"LDAPs connection to {ldap_url} failed - {e}")
175+
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
176176
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
177-
self.logger.debug("Even if the port is open, LDAPS may not be configured")
177+
self.logger.fail("Even if the port is open, LDAPS may not be configured")
178178
else:
179-
self.logger.debug(f"LDAP connection to {ldap_url} failed: {e}")
180-
return [None, None, None]
179+
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
180+
exit(1)
181181

182182
resp = ldap_connection.search(
183183
scope=ldapasn1_impacket.Scope("baseObject"),

nxc/protocols/ldap/laps.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import binascii
2-
import hashlib
31
from json import loads
42
from pyasn1.codec.der import decoder
53
from pyasn1_modules import rfc5652
@@ -37,12 +35,13 @@ def __init__(self, host, port, hostname):
3735
def proto_logger(self, host, port, hostname):
3836
self.logger = NXCAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
3937

40-
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
38+
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False, dns_server=""):
4139
lmhash = ""
4240
nthash = ""
4341

44-
if kdcHost is None:
45-
kdcHost = domain
42+
if kdcHost is None or domain not in kdcHost:
43+
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
44+
exit(1)
4645

4746
# This checks to see if we didn't provide the LM Hash
4847
if ntlm_hash and ntlm_hash.find(":") != -1:
@@ -54,12 +53,13 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
5453
baseDN = ""
5554
domainParts = domain.split(".")
5655
for i in domainParts:
57-
baseDN += f"dc={i},"
56+
baseDN += f"DC={i},"
5857
# Remove last ','
5958
baseDN = baseDN[:-1]
6059

6160
try:
62-
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
61+
self.logger.info(f"Connecting to ldap://{kdcHost} - {baseDN} - {domain} [1]")
62+
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN, dns_server if dns_server else domain)
6363
ldap_connection.kerberosLogin(
6464
username,
6565
password,
@@ -78,7 +78,7 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
7878
if str(e).find("strongerAuthRequired") >= 0:
7979
# We need to try SSL
8080
try:
81-
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
81+
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN, dns_server if dns_server else domain)
8282
ldap_connection.login(
8383
username,
8484
password,
@@ -105,18 +105,14 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
105105
color="magenta" if error_code in ldap_error_status else "red",
106106
)
107107
return False
108-
109108
except OSError:
110109
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
111110
return False
112111
except KerberosError as e:
113-
self.logger.fail(
114-
f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}",
115-
color="red",
116-
)
112+
self.logger.fail(f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}", color="red")
117113
return False
118114

119-
def auth_login(self, domain, username, password, ntlm_hash):
115+
def auth_login(self, domain, username, password, ntlm_hash, dns_server):
120116
lmhash = ""
121117
nthash = ""
122118

@@ -135,7 +131,7 @@ def auth_login(self, domain, username, password, ntlm_hash):
135131
base_dn = base_dn[:-1]
136132

137133
try:
138-
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
134+
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
139135
ldap_connection.login(username, password, domain, lmhash, nthash)
140136

141137
# Connect to LDAP
@@ -148,7 +144,7 @@ def auth_login(self, domain, username, password, ntlm_hash):
148144
if str(e).find("strongerAuthRequired") >= 0:
149145
# We need to try SSL
150146
try:
151-
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
147+
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, dns_server if dns_server else domain)
152148
ldap_connection.login(username, password, domain, lmhash, nthash)
153149
self.logger.extra["protocol"] = "LDAPS"
154150
self.logger.extra["port"] = "636"
@@ -173,7 +169,7 @@ def auth_login(self, domain, username, password, ntlm_hash):
173169

174170

175171
class LAPSv2Extract:
176-
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
172+
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
177173
if ntlm_hash.find(":") != -1:
178174
self.lmhash, self.nthash = ntlm_hash.split(":")
179175
else:
@@ -187,6 +183,7 @@ def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdc
187183
self.do_kerberos = do_kerberos
188184
self.kdcHost = kdcHost
189185
self.logger = None
186+
self.dns_server = dns_server
190187
self.proto_logger(self.domain, port, self.domain)
191188

192189
def proto_logger(self, host, port, hostname):
@@ -218,7 +215,7 @@ def run(self):
218215
gke = kds_cache[key_id["RootKeyId"]]
219216
else:
220217
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
221-
string_binding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
218+
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
222219
rpc_transport = transport.DCERPCTransportFactory(string_binding)
223220
if hasattr(rpc_transport, "set_credentials"):
224221
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
@@ -263,7 +260,7 @@ def run(self):
263260
return plaintext[:-18].decode("utf-16le")
264261

265262

266-
def laps_search(self, username, password, cred_type, domain):
263+
def laps_search(self, username, password, cred_type, domain, dns_server):
267264
prev_protocol = self.logger.extra["protocol"]
268265
prev_port = self.logger.extra["port"]
269266
self.logger.extra["protocol"] = "LDAP"
@@ -274,7 +271,7 @@ def laps_search(self, username, password, cred_type, domain):
274271
if self.kerberos:
275272
if self.kdcHost is None:
276273
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
277-
return None, None, None, None
274+
return None, None, None
278275

279276
connection = ldapco.kerberos_login(
280277
domain[0],
@@ -283,18 +280,20 @@ def laps_search(self, username, password, cred_type, domain):
283280
password[0] if cred_type[0] == "hash" else "",
284281
kdcHost=self.kdcHost,
285282
aesKey=self.aesKey,
283+
dns_server=dns_server
286284
)
287285
else:
288286
connection = ldapco.auth_login(
289287
domain[0],
290288
username[0] if username else "",
291289
password[0] if cred_type[0] == "plaintext" else "",
292290
password[0] if cred_type[0] == "hash" else "",
291+
dns_server
293292
)
294293
if not connection:
295294
self.logger.fail(f"LDAP connection failed with account {username[0]}")
296295

297-
return None, None, None, None
296+
return None, None, None
298297

299298
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
300299
attributes = [
@@ -325,13 +324,14 @@ def laps_search(self, username, password, cred_type, domain):
325324
password[0] if cred_type[0] == "hash" else "",
326325
self.kerberos,
327326
self.kdcHost,
328-
339
327+
339,
328+
dns_server
329329
)
330330
try:
331331
data = d.run()
332332
except Exception as e:
333333
self.logger.fail(str(e))
334-
return None, None, None, None
334+
return None, None, None
335335
r = loads(data)
336336
msMCSAdmPwd = r["p"]
337337
username_laps = r["n"]
@@ -346,21 +346,17 @@ def laps_search(self, username, password, cred_type, domain):
346346
self.logger.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
347347
else:
348348
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
349-
return None, None, None, None
349+
return None, None, None
350350

351351
if msMCSAdmPwd == "":
352352
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
353-
return None, None, None, None
354-
355-
hash_ntlm = None
356-
if cred_type[0] == "hash":
357-
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
358-
hash_ntlm = binascii.hexlify(hash_ntlm).decode()
353+
return None, None, None
359354

360355
username = username_laps if username_laps else self.args.laps
361356
password = msMCSAdmPwd
362357
domain = self.hostname
363358
self.args.local_auth = True
359+
self.args.kerberos = False
364360
self.logger.extra["protocol"] = prev_protocol
365361
self.logger.extra["port"] = prev_port
366-
return username, password, domain, hash_ntlm
362+
return username, password, domain

nxc/protocols/ldap/proto_args.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ def proto_args(parser, parents):
3333

3434
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
3535
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
36-
bgroup.add_argument("-c", "--collection", default="Collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
36+
bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
3737

3838
return parser

nxc/protocols/smb.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ def enum_host_info(self):
215215
if "STATUS_NOT_SUPPORTED" in str(e):
216216
# no ntlm supported
217217
self.no_ntlm = True
218+
self.logger.debug("NTLM not supported")
218219

219220
# self.domain is the attribute we authenticate with
220221
# self.targetDomain is the attribute which gets displayed as host domain
@@ -224,8 +225,20 @@ def enum_host_info(self):
224225
if not self.targetDomain: # Not sure if that can even happen but now we are safe
225226
self.targetDomain = self.hostname
226227
else:
227-
self.hostname = self.host
228-
self.targetDomain = self.hostname
228+
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
229+
try:
230+
import socket
231+
socket.inet_aton(self.host)
232+
self.logger.fail("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
233+
self.hostname = self.host
234+
self.targetDomain = self.host
235+
except OSError:
236+
if self.host.count(".") >= 1:
237+
self.hostname = self.host.split(".")[0]
238+
self.targetDomain = ".".join(self.host.split(".")[1:])
239+
else:
240+
self.hostname = self.host
241+
self.targetDomain = self.host
229242

230243
self.domain = self.targetDomain if not self.args.domain else self.args.domain
231244

@@ -236,10 +249,14 @@ def enum_host_info(self):
236249
# As of June 2024 Samba will always report the version as "Windows 6.1", apparently due to a bug https://stackoverflow.com/a/67577401/17395725
237250
# Together with the reported build version "0" by Samba we can assume that it is a Samba server. Windows should always report a build version > 0
238251
# Also only on Windows we should get an OS arch as for that we would need MSRPC
239-
self.server_os = self.conn.getServerOS()
240-
self.server_os_major = self.conn.getServerOSMajor()
241-
self.server_os_minor = self.conn.getServerOSMinor()
242-
self.server_os_build = self.conn.getServerOSBuild()
252+
try:
253+
self.server_os = self.conn.getServerOS()
254+
self.server_os_major = self.conn.getServerOSMajor()
255+
self.server_os_minor = self.conn.getServerOSMinor()
256+
self.server_os_build = self.conn.getServerOSBuild()
257+
except KeyError:
258+
self.logger.debug("Error getting server information...")
259+
243260
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
244261
self.server_os = "Unix - Samba"
245262
elif self.server_os_build == 0 and self.os_arch == 0:
@@ -259,14 +276,17 @@ def enum_host_info(self):
259276
self.os_arch = self.get_os_arch()
260277
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
261278

262-
self.db.add_host(
263-
self.host,
264-
self.hostname,
265-
self.domain,
266-
self.server_os,
267-
self.smbv1,
268-
self.signing,
269-
)
279+
try:
280+
self.db.add_host(
281+
self.host,
282+
self.hostname,
283+
self.domain,
284+
self.server_os,
285+
self.smbv1,
286+
self.signing,
287+
)
288+
except Exception as e:
289+
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
270290

271291
try:
272292
# DCs seem to want us to logoff first, windows workstations sometimes reset the connection
@@ -713,11 +733,12 @@ def execute(self, payload=None, get_output=False, methods=None):
713733
self.logger.fail("Command execution blocked by AMSI")
714734
return None
715735

716-
if (self.args.execute or self.args.ps_execute) and output:
736+
if (self.args.execute or self.args.ps_execute):
717737
self.logger.success(f"Executed command via {current_method}")
718-
output_lines = StringIO(output).readlines()
719-
for line in output_lines:
720-
self.logger.highlight(line.strip())
738+
if output:
739+
output_lines = StringIO(output).readlines()
740+
for line in output_lines:
741+
self.logger.highlight(line.strip())
721742
return output
722743
else:
723744
self.logger.fail(f"Execute command failed with {current_method}")

nxc/protocols/smb/mmcexec.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def __init__(self, target, share_name, username, password, domain, smbconnection
103103
)
104104
try:
105105
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
106-
except Exception:
106+
except Exception as e:
107+
self.logger.info(f"Got Exception while connecting with DCOM: {e}")
107108
# Make it force break function
108109
self.__dcom.disconnect()
109110
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)

0 commit comments

Comments
 (0)