Skip to content

Commit 16508f8

Browse files
Merge branch 'main' into marshall-options-fix
Signed-off-by: Marshall Hallenbeck <Marshall.Hallenbeck@gmail.com>
2 parents 152958e + cf231d5 commit 16508f8

43 files changed

Lines changed: 701 additions & 439 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.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
data/nxc.db
22
hash_spider_default.sqlite3
3+
hash_spider_testing.sqlite3
34
*.bak
45
*.log
56
.venv

nxc/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ def gen_cli_args():
6464

6565
parser.add_argument("--version", action="store_true", help="Display nxc version")
6666

67+
dns_parser = parser.add_argument_group("DNS")
68+
dns_parser.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
69+
dns_parser.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
70+
dns_parser.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
71+
dns_parser.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds (default: %(default)s)")
72+
6773
# we do module arg parsing here so we can reference the module_list attribute below
6874
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
6975
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
@@ -76,7 +82,6 @@ def gen_cli_args():
7682

7783
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser], formatter_class=DisplayDefaultsNotNone)
7884
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) 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)")
79-
8085
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
8186
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
8287
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")

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)