Skip to content

Commit 8a0dcf0

Browse files
authored
Merge branch 'main' into passwords_dump_update
Signed-off-by: zblurx <68540460+zblurx@users.noreply.github.com>
2 parents dd7cbb6 + cf231d5 commit 8a0dcf0

54 files changed

Lines changed: 1159 additions & 603 deletions

Some content is hidden

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

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
---
2+
name: Feature request
3+
about: Request a new feature or enhancement
4+
title: ''
5+
labels: ''
6+
assignees: ''
7+
8+
---
19
**Please Describe The Problem To Be Solved**
210
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
311

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
name: Pull request
3+
about: Update code to fix a bug or add an enhancement/feature
4+
title: ''
5+
labels: ''
6+
assignees: ''
7+
8+
---
9+
## Description
10+
11+
Please include a summary of the change and which issue is fixed, or what the enhancement does.
12+
Please also include relevant motivation and context.
13+
List any dependencies that are required for this change.
14+
15+
## Type of change
16+
Please delete options that are not relevant.
17+
- [ ] Bug fix (non-breaking change which fixes an issue)
18+
- [ ] New feature (non-breaking change which adds functionality)
19+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
20+
- [ ] This change requires a documentation update
21+
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
22+
23+
## How Has This Been Tested?
24+
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
25+
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
26+
27+
If you are using poetry, you can easily run tests via:
28+
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
29+
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
30+
31+
## Screenshots (if appropriate):
32+
Screenshots are always nice to have and can give a visual representation of the change.
33+
If appropriate include before and after screenshot(s) to show which results are to be expected.
34+
35+
## Checklist:
36+
37+
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
38+
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
39+
- [ ] New and existing e2e tests pass locally with my changes
40+
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
41+
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
42+
- [ ] I have performed a self-review of my own code
43+
- [ ] I have commented my code, particularly in hard-to-understand areas
44+
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ jobs:
2626
python-version: ${{ matrix.python-version }}
2727
cache: poetry
2828
cache-dependency-path: poetry.lock
29+
- name: Install with pipx
30+
run: |
31+
pipx install . --python python${{ matrix.python-version }}
2932
- name: Install poetry
3033
run: |
3134
pipx install poetry --python python${{ matrix.python-version }}
@@ -45,4 +48,4 @@ jobs:
4548
poetry run netexec mssql 127.0.0.1
4649
poetry run netexec ssh 127.0.0.1
4750
poetry run netexec ftp 127.0.0.1
48-
poetry run netexec smb 127.0.0.1 -M veeam
51+
poetry run netexec smb 127.0.0.1 -M veeam

.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

netexec.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ a = Analysis(
4242
'nxc.helpers.bash',
4343
'nxc.helpers.bloodhound',
4444
'nxc.helpers.msada_guids',
45+
'nxc.helpers.ntlm_parser',
4546
'paramiko',
4647
'pypsrp.client',
4748
'pywerview.cli.helpers',

nxc/cli.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ def gen_cli_args():
4646

4747
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
4848
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
49-
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
49+
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication (default: None)")
5050
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
5151
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
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: 106 additions & 40 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,57 +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

111-
if args.jitter:
112-
jitter = args.jitter
113-
if "-" in jitter:
114-
start, end = jitter.split("-")
115-
jitter = (int(start), int(end))
116-
else:
117-
jitter = (0, int(jitter))
157+
if self.args.kerberos:
158+
self.host = self.hostname
118159

119-
value = random.choice(range(jitter[0], jitter[1]))
120-
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
121-
sleep(value)
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}")
122161

123162
try:
124163
self.proto_flow()
125164
except Exception as e:
126165
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
127-
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}")
128167
else:
129-
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}")
130169
finally:
131-
self.logger.debug(f"Closing connection to: {host}")
170+
self.logger.debug(f"Closing connection to: {target}")
132171
with contextlib.suppress(Exception):
133172
self.conn.close()
134173

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+
135187
@staticmethod
136188
def proto_args(std_parser, module_parser):
137189
return
@@ -388,7 +440,9 @@ def parse_credentials(self):
388440
return domain, username, owned, secret, cred_type, [None] * len(secret)
389441

390442
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
391-
"""Try to login using the specified credentials and protocol.
443+
"""
444+
Try to login using the specified credentials and protocol.
445+
With --jitter an authentication throttle can be applied.
392446
393447
Possible login methods are:
394448
- plaintext (/kerberos)
@@ -401,6 +455,18 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
401455
return False
402456
if hasattr(self.args, "delegate") and self.args.delegate:
403457
self.args.kerberos = True
458+
459+
if self.args.jitter:
460+
jitter = self.args.jitter
461+
if "-" in jitter:
462+
start, end = jitter.split("-")
463+
jitter = (int(start), int(end))
464+
else:
465+
jitter = (0, int(jitter))
466+
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
467+
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
468+
sleep(value)
469+
404470
with sem:
405471
if cred_type == "plaintext":
406472
if self.args.kerberos:

nxc/helpers/bloodhound.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,8 @@ def add_user_bh(user, domain, logger, config):
5252
_add_with_domain(user_info, domain, tx, logger)
5353
except AuthError:
5454
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
55-
exit()
5655
except ServiceUnavailable:
5756
logger.fail(f"Neo4J does not seem to be available on {uri}.")
58-
exit()
5957
except Exception as e:
6058
logger.fail(f"Unexpected error with Neo4J: {e}")
6159
finally:

0 commit comments

Comments
 (0)