Skip to content

Commit 1afef2f

Browse files
authored
Merge branch 'main' into be
Signed-off-by: mpgn <5891788+mpgn@users.noreply.github.com>
2 parents 7091022 + 1281b0f commit 1afef2f

4 files changed

Lines changed: 129 additions & 36 deletions

File tree

nxc/helpers/misc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import inspect
55
import os
66

7+
from ipaddress import ip_address
78

89
def identify_target_file(target_file):
910
with open(target_file) as target_file_handle:
@@ -78,7 +79,6 @@ def _access_check(fn, mode):
7879
if _access_check(name, mode):
7980
return name
8081

81-
8282
def get_bloodhound_info():
8383
"""
8484
Detect which BloodHound package is installed (regular or CE) and its version.
@@ -135,3 +135,11 @@ def get_bloodhound_info():
135135
pass
136136

137137
return package_name, version, is_ce
138+
139+
def detect_if_ip(target):
140+
try:
141+
ip_address(target)
142+
return True
143+
except Exception:
144+
return False
145+

nxc/modules/eventlog_creds.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ def __init__(self):
2020
self.context = None
2121
self.module_options = None
2222
self.method = "execute"
23-
self.limit = 1000
23+
self.limit = None
2424

2525
def options(self, context, module_options):
2626
"""
27-
METHOD EventLog method (Execute or RPCCALL)
27+
METHOD EventLog method (Execute or RPCCALL), default: execute
2828
M Alias for METHOD
29-
LIMIT Limit of the number of records to be fetched
29+
LIMIT Limit of the number of records to be fetched, default: unlimited
3030
L Alias for LIMIT
3131
"""
3232
if "METHOD" in module_options:
@@ -41,8 +41,6 @@ def options(self, context, module_options):
4141
def find_credentials(self, content, context):
4242
# remove unnecessary words
4343
content = content.replace("\r\n", "\n")
44-
content = content.replace("/add", "")
45-
content = content.replace("/active:yes", "")
4644

4745
# sort and unique lines
4846
content = "\n".join(sorted(set(content.split("\n"))))
@@ -66,9 +64,16 @@ def find_credentials(self, content, context):
6664
# Extracting credentials
6765
for line in content.split("\n"):
6866
for reg in regexps:
69-
# verbose context.log.debug("Line: " + line)
70-
# verbose context.log.debug("Reg: " + reg)
71-
match = re.search(reg, line, re.IGNORECASE)
67+
# Remove unnecessary words
68+
line_stripped = line.replace("/add", "") \
69+
.replace("/active:yes", "") \
70+
.replace("/delete", "") \
71+
.replace("/domain", "") \
72+
# Remove command lines that were executed with nxc
73+
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
74+
75+
# Use regex to find credentials
76+
match = re.search(reg, line_stripped, re.IGNORECASE)
7277
if match:
7378
# eleminate false positives
7479
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
@@ -92,11 +97,12 @@ def find_credentials(self, content, context):
9297

9398
def on_admin_login(self, context, connection):
9499
content = ""
95-
if self.method[:1].lower() == "e":
100+
if self.method.lower().startswith("e"):
101+
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
96102
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
97103
commands = [
98-
f'wevtutil qe Security /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=4688)]]" |findstr "Command Line"',
99-
f'wevtutil qe Microsoft-Windows-Sysmon/Operational /c:{self.limit} /f:text /rd:true /q:"*[System[(EventID=1)]]" |findstr "ParentCommandLine"'
104+
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
105+
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
100106
]
101107
for command in commands:
102108
context.log.debug("Execute Command: " + command)
@@ -127,7 +133,6 @@ def on_admin_login(self, context, connection):
127133
content += "CommandLine: " + match.group("CommandLine") + "\n"
128134
except Exception as e:
129135
context.log.error(f"Error: {e}")
130-
continue
131136

132137
self.find_credentials(content, context)
133138

@@ -182,7 +187,7 @@ def query(self, path, query, limit):
182187

183188

184189
class MSEven6Result:
185-
def __init__(self, conn, handle, limit):
190+
def __init__(self, conn, handle, limit=None):
186191
self._conn = conn
187192
self._handle = handle
188193
self._hardlimit = limit
@@ -192,11 +197,12 @@ def __iter__(self):
192197
return self
193198

194199
def __next__(self):
195-
self._hardlimit -= 1
196-
if self._hardlimit < 0:
197-
raise StopIteration
200+
if self._hardlimit is not None:
201+
self._hardlimit -= 1
202+
if self._hardlimit < 0:
203+
raise StopIteration
198204
if self._resp is not None and self._resp["NumActualRecords"] == 0:
199-
return None
205+
raise StopIteration
200206

201207
if self._resp is None or self._index == self._resp["NumActualRecords"]:
202208
req = even6.EvtRpcQueryNext()

nxc/protocols/ldap/resolution.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from re import sub, I
2+
from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH
3+
from OpenSSL.SSL import SysCallError
4+
5+
from impacket.ldap import ldap as ldap_impacket
6+
from impacket.ldap import ldapasn1 as ldapasn1_impacket
7+
8+
from nxc.parsers.ldap_results import parse_result_attributes
9+
from nxc.logger import nxc_logger
10+
11+
class LDAPResolution:
12+
13+
def __init__(self, host):
14+
self.host = host
15+
16+
def get_resolution(self):
17+
target = ""
18+
target_domain = ""
19+
base_dn = ""
20+
try:
21+
ldap_url = f"ldap://{self.host}"
22+
nxc_logger.info(f"Connecting to {ldap_url} with no baseDN")
23+
try:
24+
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
25+
if self.ldap_connection:
26+
nxc_logger.debug(f"ldap_connection: {self.ldap_connection}")
27+
except SysCallError as e:
28+
nxc_logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
29+
return False
30+
31+
resp = self.ldap_connection.search(
32+
scope=ldapasn1_impacket.Scope("baseObject"),
33+
attributes=["defaultNamingContext", "dnsHostName"],
34+
sizeLimit=0,
35+
)
36+
resp_parsed = parse_result_attributes(resp)[0]
37+
38+
target = resp_parsed["dnsHostName"]
39+
base_dn = resp_parsed["defaultNamingContext"]
40+
target_domain = sub(
41+
",DC=",
42+
".",
43+
base_dn[base_dn.lower().find("dc="):],
44+
flags=I,
45+
)[3:]
46+
# Extract machine name from target (hostname part of FQDN)
47+
if target:
48+
machine_name = target.split(".")[0]
49+
nxc_logger.debug(f"Extracted machine name: {machine_name}")
50+
51+
self.ldap_connection.close()
52+
except ConnectionRefusedError as e:
53+
nxc_logger.debug(f"{e} on host {self.host}")
54+
return False
55+
except OSError as e:
56+
if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT):
57+
nxc_logger.info(f"Error connecting to {self.host} - {e}")
58+
return False
59+
else:
60+
nxc_logger.error(f"Error getting ldap info {e}")
61+
62+
nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}")
63+
return machine_name, target_domain

nxc/protocols/smb.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
from nxc.helpers.logger import highlight
5656
from nxc.helpers.bloodhound import add_user_bh
5757
from nxc.helpers.powershell import create_ps_command
58+
from nxc.helpers.misc import detect_if_ip
59+
from nxc.protocols.ldap.resolution import LDAPResolution
5860

5961
from dploot.triage.vaults import VaultsTriage
6062
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
@@ -125,6 +127,7 @@ def __init__(self, args, db, host):
125127
self.no_ntlm = False
126128
self.protocol = "SMB"
127129
self.is_guest = None
130+
self.isdc = False
128131

129132
connection.__init__(self, args, db, host)
130133

@@ -185,20 +188,30 @@ def enum_host_info(self):
185188
if not self.targetDomain: # Not sure if that can even happen but now we are safe
186189
self.targetDomain = self.hostname
187190
else:
188-
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
189191
try:
190-
import socket
191-
socket.inet_aton(self.host)
192-
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
193-
self.hostname = self.host
194-
self.targetDomain = self.host
192+
# If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available
193+
if self.is_host_dc() and detect_if_ip(self.host):
194+
self.hostname, self.domain = LDAPResolution(self.host).get_resolution()
195+
self.targetDomain = self.domain
196+
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
197+
else:
198+
# Check if the host is a valid IP address, if not we parse the FQDN in the Exception
199+
import socket
200+
socket.inet_aton(self.host)
201+
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
202+
self.hostname = self.host
203+
self.targetDomain = self.host
195204
except OSError:
196205
if self.host.count(".") >= 1:
197206
self.hostname = self.host.split(".")[0]
198207
self.targetDomain = ".".join(self.host.split(".")[1:])
199208
else:
200209
self.hostname = self.host
201210
self.targetDomain = self.host
211+
except Exception as e:
212+
self.logger.debug(f"Error getting hostname from LDAP: {e}")
213+
self.hostname = self.host
214+
self.targetDomain = self.host
202215

203216
if self.args.domain:
204217
self.domain = self.args.domain
@@ -283,21 +296,12 @@ def print_host_info(self):
283296
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}) {ntlm}")
284297

285298
if self.args.generate_hosts_file or self.args.generate_krb5_file:
286-
from impacket.dcerpc.v5 import nrpc, epm
287-
self.logger.debug("Performing authentication attempts...")
288-
isdc = False
289-
try:
290-
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
291-
isdc = True
292-
except DCERPCException:
293-
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
294-
295299
if self.args.generate_hosts_file:
296300
with open(self.args.generate_hosts_file, "a+") as host_file:
297-
dc_part = f" {self.targetDomain}" if isdc else ""
301+
dc_part = f" {self.targetDomain}" if self.isdc else ""
298302
host_file.write(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}\n")
299-
self.logger.debug(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}")
300-
elif self.args.generate_krb5_file and isdc:
303+
self.logger.debug(f"Line added to {self.args.generate_hosts_file} {self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}")
304+
elif self.args.generate_krb5_file and self.isdc:
301305
with open(self.args.generate_krb5_file, "w+") as host_file:
302306
data = f"""
303307
[libdefaults]
@@ -658,6 +662,18 @@ def generate_tgt(self):
658662
except Exception as e:
659663
self.logger.fail(f"Failed to get TGT: {e}")
660664

665+
def is_host_dc(self):
666+
from impacket.dcerpc.v5 import nrpc, epm
667+
self.logger.debug("Performing authentication attempts...")
668+
try:
669+
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
670+
self.isdc = True
671+
return True
672+
except DCERPCException:
673+
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
674+
self.isdc = False
675+
return False
676+
661677
@requires_admin
662678
def execute(self, payload=None, get_output=False, methods=None) -> str:
663679
"""

0 commit comments

Comments
 (0)