Skip to content

Commit b1238fc

Browse files
authored
Merge branch 'Pennyw0rth:main' into timeroast_module
2 parents e7d3032 + cf231d5 commit b1238fc

42 files changed

Lines changed: 707 additions & 436 deletions

Some content is hidden

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

nxc/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ def gen_cli_args():
5252
parser.add_argument("--debug", action="store_true", help="enable debug level information")
5353
parser.add_argument("--version", action="store_true", help="Display nxc version")
5454

55+
dns_parser = parser.add_argument_group("DNS")
56+
dns_parser.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
57+
dns_parser.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
58+
dns_parser.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
59+
dns_parser.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds (default: %(default)s)")
60+
5561
# we do module arg parsing here so we can reference the module_list attribute below
5662
module_parser = argparse.ArgumentParser(add_help=False)
5763
mgroup = module_parser.add_mutually_exclusive_group()
@@ -78,7 +84,7 @@ def gen_cli_args():
7884
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
7985
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
8086
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
81-
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
87+
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
8288

8389
fail_group = std_parser.add_mutually_exclusive_group()
8490
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")

nxc/connection.py

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from functools import wraps
55
from time import sleep
66
from ipaddress import ip_address
7+
from dns import resolver, rdatatype
78
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
89

910
from nxc.config import pwned_label
@@ -22,23 +23,63 @@
2223
user_failed_logins = {}
2324

2425

25-
def gethost_addrinfo(hostname):
26-
is_ipv6 = False
27-
is_link_local_ipv6 = False
26+
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
27+
result = {
28+
"host": "",
29+
"is_ipv6": False,
30+
"is_link_local_ipv6": False
31+
}
2832
address_info = {"AF_INET6": "", "AF_INET": ""}
2933

30-
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
31-
af, _, _, canonname, sa = res
32-
address_info[af.name] = sa[0]
34+
try:
35+
if ip_address(target).version == 4:
36+
address_info["AF_INET"] = target
37+
else:
38+
address_info["AF_INET6"] = target
39+
except Exception:
40+
# If the target is not an IP address, we need to resolve it
41+
if not (dns_server or dns_tcp):
42+
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
43+
af, _, _, canonname, sa = res
44+
address_info[af.name] = sa[0]
45+
46+
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
47+
address_info["AF_INET6"] = canonname
48+
result["is_link_local_ipv6"] = True
49+
else:
50+
dnsresolver = resolver.Resolver()
51+
dnsresolver.timeout = dns_timeout
52+
dnsresolver.lifetime = dns_timeout
53+
54+
if dns_server:
55+
dnsresolver.nameservers = [dns_server]
56+
57+
try:
58+
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
59+
address_info["AF_INET"] = answers_ipv4[0].address
60+
except Exception:
61+
pass
62+
63+
try:
64+
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
65+
address_info["AF_INET6"] = answers_ipv6[0].address
66+
67+
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
68+
result["is_link_local_ipv6"] = True
69+
except Exception:
70+
pass
71+
72+
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
73+
raise Exception(f"The DNS query name does not exist: {target}")
3374

3475
# IPv4 preferred
35-
if address_info["AF_INET"]:
36-
host = address_info["AF_INET"]
76+
if address_info["AF_INET"] and not force_ipv6:
77+
result["host"] = address_info["AF_INET"]
3778
else:
38-
is_ipv6 = True
39-
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
79+
result["is_ipv6"] = True
80+
result["host"] = address_info["AF_INET6"]
4081

41-
return host, is_ipv6, is_link_local_ipv6
82+
return result
4283

4384

4485
def requires_admin(func):
@@ -50,7 +91,7 @@ def _decorator(self, *args, **kwargs):
5091
return wraps(func)(_decorator)
5192

5293

53-
def dcom_FirewallChecker(iInterface, timeout):
94+
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
5495
stringBindings = iInterface.get_cinstance().get_string_bindings()
5596
for strBinding in stringBindings:
5697
if strBinding["wTowerId"] == 7:
@@ -70,6 +111,7 @@ def dcom_FirewallChecker(iInterface, timeout):
70111
return True, None
71112
try:
72113
rpctransport = transport.DCERPCTransportFactory(stringBinding)
114+
rpctransport.setRemoteHost(remoteHost)
73115
rpctransport.set_connect_timeout(timeout)
74116
rpctransport.connect()
75117
rpctransport.disconnect()
@@ -81,45 +123,67 @@ def dcom_FirewallChecker(iInterface, timeout):
81123

82124

83125
class connection:
84-
def __init__(self, args, db, host):
85-
self.domain = None
126+
def __init__(self, args, db, target):
86127
self.args = args
87128
self.db = db
88-
self.hostname = host
89-
self.port = self.args.port
129+
self.logger = nxc_logger
90130
self.conn = None
91-
self.admin_privs = False
131+
132+
# Authentication info
92133
self.password = ""
93134
self.username = ""
94135
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
95136
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
96-
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
97137
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
138+
self.admin_privs = False
98139
self.failed_logins = 0
140+
141+
# Network info
142+
self.domain = None
143+
self.host = None # IP address of the target. If kerberos this is the hostname
144+
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
145+
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
146+
self.kdcHost = self.args.kdcHost
147+
self.port = self.args.port
99148
self.local_ip = None
100-
self.logger = nxc_logger
101149

102-
try:
103-
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
104-
if self.args.kerberos:
105-
self.host = self.hostname
106-
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
107-
except Exception as e:
108-
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
150+
# DNS resolution
151+
dns_result = self.resolver(target)
152+
if dns_result:
153+
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
154+
else:
109155
return
110156

157+
if self.args.kerberos:
158+
self.host = self.hostname
159+
160+
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
161+
111162
try:
112163
self.proto_flow()
113164
except Exception as e:
114165
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
115-
self.logger.error(f"Exception while calling proto_flow() on target {self.host}: {e}")
166+
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
116167
else:
117-
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
168+
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
118169
finally:
119-
self.logger.debug(f"Closing connection to: {host}")
170+
self.logger.debug(f"Closing connection to: {target}")
120171
with contextlib.suppress(Exception):
121172
self.conn.close()
122173

174+
def resolver(self, target):
175+
try:
176+
return get_host_addr_info(
177+
target=target,
178+
force_ipv6=self.args.force_ipv6,
179+
dns_server=self.args.dns_server,
180+
dns_tcp=self.args.dns_tcp,
181+
dns_timeout=self.args.dns_timeout
182+
)
183+
except Exception as e:
184+
self.logger.info(f"Error resolving hostname {target}: {e}")
185+
return None
186+
123187
@staticmethod
124188
def proto_args(std_parser, module_parser):
125189
return

nxc/helpers/powershell.py

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212

1313
obfuscate_ps_scripts = False
1414

15+
def replace_singles(s):
16+
"""Replaces single quotes with a double quote
17+
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
18+
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
19+
20+
Args:
21+
----
22+
s (str): The string to replace single quotes in.
23+
24+
Returns:
25+
-------
26+
str: Original string with single quotes replaced with double.
27+
"""
28+
return s.replace("'", r"\"")
1529

1630
def get_ps_script(path):
1731
"""Generates a full path to a PowerShell script given a relative path.
@@ -108,91 +122,66 @@ def obfs_ps_script(path_to_script):
108122

109123

110124

111-
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
125+
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
112126
"""
113127
Generates a PowerShell command based on the provided `ps_command` parameter.
114128
115129
Args:
116130
----
117131
ps_command (str): The PowerShell command to be executed.
118-
119132
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
120-
121-
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
122-
133+
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
123134
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
135+
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
124136
125137
Returns:
126138
-------
127139
str: The generated PowerShell command.
128140
"""
141+
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
142+
129143
if custom_amsi:
144+
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
130145
with open(custom_amsi) as file_in:
131146
lines = list(file_in)
132147
amsi_bypass = "".join(lines)
133148
else:
134-
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
135-
try{
136-
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
137-
}catch{}
138-
"""
139-
140-
command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
149+
amsi_bypass = ""
141150

151+
# for readability purposes, we do not do a one-liner
152+
if force_ps32: # noqa: SIM108
153+
# https://stackoverflow.com/a/60155248
154+
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
155+
else:
156+
command = f"{amsi_bypass} {ps_command}"
157+
142158
nxc_logger.debug(f"Generated PS command:\n {command}\n")
143159

144-
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
145-
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
146-
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
147-
# (https://github.com/GreatSCT/GreatSCT) <3
148-
149-
"""
150-
if is_powershell_installed():
151-
152-
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
153-
suffix='.ps1',
154-
dir='/tmp')
155-
temp.write(command)
156-
temp.read()
157-
158-
encoding_types = [1,2,3,4,5,6]
159-
while True:
160-
encoding = random.choice(encoding_types)
161-
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
162-
temp.name,
163-
encoding)
164-
nxc_logger.debug(invoke_obfs_command)
165-
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
166-
167-
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
168-
nxc_logger.debug('Command length: {}'.format(len(command)))
169-
170-
if len(command) <= 8192:
171-
temp.close()
172-
break
173-
174-
encoding_types.remove(encoding)
175-
176-
else:
177-
"""
178-
if not dont_obfs:
160+
if obfs:
161+
nxc_logger.debug("Obfuscating PowerShell command")
179162
obfs_attempts = 0
180163
while True:
181-
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
164+
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
165+
obfs_command = invoke_obfuscation(command)
166+
167+
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
182168
if len(command) <= 8191:
183169
break
184-
185170
if obfs_attempts == 4:
186171
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
187172
exit(1)
188-
173+
nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
189174
obfs_attempts += 1
190175
else:
191-
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
176+
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
177+
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
178+
command = f"powershell.exe -noni -nop -w 1 {command}"
179+
192180
if len(command) > 8191:
193181
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
194182
exit(1)
195-
183+
184+
nxc_logger.debug(f"Final command: {command}")
196185
return command
197186

198187

@@ -320,6 +309,7 @@ def invoke_obfuscation(script_string):
320309
-------
321310
str: The obfuscated payload for execution.
322311
"""
312+
nxc_logger.debug(f"Command before obfuscation: {script_string}")
323313
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
324314
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
325315

@@ -436,5 +426,7 @@ def invoke_obfuscation(script_string):
436426
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
437427
]
438428

439-
return choice(invoke_options)
429+
obfuscated_script = choice(invoke_options)
430+
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
431+
return obfuscated_script
440432

nxc/logger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def __init__(self, extra=None):
9393
self.logger = logging.getLogger("nxc")
9494
self.extra = extra
9595
self.output_file = None
96-
96+
97+
logging.getLogger("impacket").disabled = True
9798
logging.getLogger("pypykatz").disabled = True
9899
logging.getLogger("minidump").disabled = True
99100
logging.getLogger("lsassy").disabled = True

0 commit comments

Comments
 (0)