Skip to content

Commit b250e1d

Browse files
authored
Merge branch 'main' into module_dpapihash
2 parents be9ee13 + a5ec90e commit b250e1d

113 files changed

Lines changed: 7004 additions & 3203 deletions

Some content is hidden

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

.github/PULL_REQUEST_TEMPLATE/pull_request_template.md renamed to .github/PULL_REQUEST_TEMPLATE.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
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-
---
91
## Description
102

113
Please include a summary of the change and which issue is fixed, or what the enhancement does.

.github/workflows/build-binaries.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ jobs:
1010
strategy:
1111
matrix:
1212
os: [ubuntu-latest, macOS-latest, windows-latest]
13-
python-version: ["3.11"]
13+
python-version: ["3.12"]
1414
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
1515
steps:
16-
- uses: actions/checkout@v3
16+
- uses: actions/checkout@v4
1717
- name: NetExec set up python on ${{ matrix.os }}
18-
uses: actions/setup-python@v4
18+
uses: actions/setup-python@v5
1919
with:
2020
python-version: ${{ matrix.python-version }}
2121
- name: Build Native Binary
@@ -25,13 +25,13 @@ jobs:
2525
pyinstaller netexec.spec
2626
- name: Upload Windows Binary
2727
if: runner.os == 'windows'
28-
uses: actions/upload-artifact@v3
28+
uses: actions/upload-artifact@v4
2929
with:
3030
name: nxc.exe
3131
path: dist/nxc.exe
3232
- name: Upload Nix/OSx Binary
3333
if: runner.os != 'windows'
34-
uses: actions/upload-artifact@v3
34+
uses: actions/upload-artifact@v4
3535
with:
3636
name: nxc-${{ matrix.os }}
3737
path: dist/nxc

.github/workflows/build-zipapps.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ jobs:
1010
strategy:
1111
matrix:
1212
os: [ubuntu-latest, macOS-latest, windows-latest]
13-
python-version: ["3.8", "3.9", "3.10", "3.11"]
13+
python-version: ["3.10", "3.11", "3.12"]
1414
steps:
15-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v4
1616
- name: NetExec set up python on ${{ matrix.os }}
17-
uses: actions/setup-python@v4
17+
uses: actions/setup-python@v5
1818
with:
1919
python-version: ${{ matrix.python-version }}
2020
- name: Build Python ZipApp with Shiv
2121
run: |
2222
pip install shiv
2323
python build_collector.py
2424
- name: Upload nxc ZipApp
25-
uses: actions/upload-artifact@v3
25+
uses: actions/upload-artifact@v4
2626
with:
2727
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
2828
path: bin/nxc
2929
- name: Upload nxcdb ZipApp
30-
uses: actions/upload-artifact@v3
30+
uses: actions/upload-artifact@v4
3131
with:
3232
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
3333
path: bin/nxcdb

.github/workflows/lint.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: Lint Python code with ruff
33

44
on:
55
push:
6+
workflow_dispatch:
67

78
jobs:
89
lint:
@@ -11,14 +12,14 @@ jobs:
1112
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
1213

1314
steps:
14-
- uses: actions/checkout@v3
15+
- uses: actions/checkout@v4
1516
- name: Install poetry
1617
run: |
1718
pipx install poetry
1819
- name: Set up Python
19-
uses: actions/setup-python@v4
20+
uses: actions/setup-python@v5
2021
with:
21-
python-version: 3.11
22+
python-version: 3.12
2223
cache: poetry
2324
cache-dependency-path: poetry.lock
2425
- name: Install dependencies with dev group

.github/workflows/test.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ on:
88
jobs:
99
build:
1010
name: Test for Py${{ matrix.python-version }}
11-
if: github.event.review.state == 'APPROVED'
11+
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
1212
runs-on: ${{ matrix.os }}
1313
strategy:
1414
max-parallel: 5
1515
matrix:
1616
os: [ubuntu-latest]
17-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
17+
python-version: ["3.10", "3.11", "3.12"]
1818
steps:
19-
- uses: actions/checkout@v3
19+
- uses: actions/checkout@v4
2020
- name: Install poetry
2121
run: |
22-
pipx install poetry
22+
pipx install poetry==1.8.4
2323
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
24-
uses: actions/setup-python@v4
24+
uses: actions/setup-python@v5
2525
with:
2626
python-version: ${{ matrix.python-version }}
2727
cache: poetry

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
1+
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
22
Copyright (c) 2022, byt3bl33d3r
33
All rights reserved.
44

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg)
1+
![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg)
22
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
33
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
44
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
@@ -43,6 +43,11 @@ sudo apt install pipx git
4343
pipx ensurepath
4444
pipx install git+https://github.com/Pennyw0rth/NetExec
4545
```
46+
47+
## Availability on Unix distributions
48+
49+
[![Packaging status](https://repology.org/badge/vertical-allrepos/netexec.svg)](https://repology.org/project/netexec/versions)
50+
4651
# Development
4752
Development guidelines and recommendations in development
4853

netexec.spec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ a = Analysis(
2525
'impacket.dcerpc.v5.lsad',
2626
'impacket.dcerpc.v5.gkdi',
2727
'impacket.dcerpc.v5.rprn',
28+
'impacket.dcerpc.v5.even',
2829
'impacket.dpapi_ng',
2930
'impacket.tds',
3031
'impacket.version',
3132
'impacket.ldap.ldap',
33+
'jwt',
3234
'nxc.connection',
3335
'nxc.servers.smb',
3436
'nxc.protocols.smb.wmiexec',
@@ -48,6 +50,7 @@ a = Analysis(
4850
'pywerview.cli.helpers',
4951
'pylnk3',
5052
'pypykatz',
53+
'pyNfsClient',
5154
'masky',
5255
'msldap',
5356
'msldap.connection',
@@ -69,6 +72,7 @@ a = Analysis(
6972
'dploot.triage.masterkeys',
7073
'dploot.triage.mobaxterm',
7174
'dploot.triage.backupkey',
75+
'dploot.triage.wam',
7276
'dploot.triage.wifi',
7377
'dploot.triage.sccm',
7478
'dploot.lib.target',

nxc/cli.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ def gen_cli_args():
2222
except ValueError:
2323
VERSION = importlib.metadata.version("netexec")
2424
COMMIT = ""
25-
CODENAME = "ItsAlwaysDNS"
25+
CODENAME = "NeedForSpeed"
2626
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
2727

2828
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
2929
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
30+
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
3031
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
3132
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
3233
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
@@ -69,8 +70,6 @@ def gen_cli_args():
6970
parents=[generic_parser, output_parser, dns_parser]
7071
)
7172

72-
parser.add_argument("--version", action="store_true", help="Display nxc version")
73-
7473
# we do module arg parsing here so we can reference the module_list attribute below
7574
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
7675
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
@@ -82,7 +81,7 @@ def gen_cli_args():
8281
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
8382

8483
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
85-
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)")
84+
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 or generic_parser.parse_known_args()[0].version) 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)")
8685
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
8786
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
8887
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
@@ -99,6 +98,13 @@ def gen_cli_args():
9998
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
10099
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
101100
kerberos_group.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")
101+
102+
certificate_group = std_parser.add_argument_group("Certificate", "Options for certificate authentication")
103+
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
104+
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
105+
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
106+
certificate_group.add_argument("--cert-pem", metavar="CERTPEM", help="Use certificate authentication from PEM file")
107+
certificate_group.add_argument("--key-pem", metavar="KEYPEM", help="Private key for the PEM format")
102108

103109
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
104110
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")

nxc/connection.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import random
2+
import sys
3+
import contextlib
4+
25
from os.path import isfile
36
from threading import BoundedSemaphore
47
from functools import wraps
@@ -13,10 +16,9 @@
1316
from nxc.logger import nxc_logger, NXCAdapter
1417
from nxc.context import Context
1518
from nxc.protocols.ldap.laps import laps_search
19+
from nxc.helpers.pfx import pfx_auth
1620

1721
from impacket.dcerpc.v5 import transport
18-
import sys
19-
import contextlib
2022

2123
sem = BoundedSemaphore(1)
2224
global_failed_logins = 0
@@ -134,7 +136,7 @@ def __init__(self, args, db, target):
134136
# Authentication info
135137
self.password = ""
136138
self.username = ""
137-
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
139+
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
138140
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
139141
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
140142
self.admin_privs = False
@@ -157,7 +159,7 @@ def __init__(self, args, db, target):
157159
else:
158160
return
159161

160-
if self.args.kerberos:
162+
if self.kerberos:
161163
self.host = self.hostname
162164

163165
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}")
@@ -167,6 +169,9 @@ def __init__(self, args, db, target):
167169
except Exception as e:
168170
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
169171
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
172+
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
173+
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
174+
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
170175
else:
171176
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
172177
finally:
@@ -203,13 +208,16 @@ def print_host_info(self):
203208
def create_conn_obj(self):
204209
return
205210

211+
def disconnect(self):
212+
return
213+
206214
def check_if_admin(self):
207215
return
208216

209217
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
210218
return
211219

212-
def plaintext_login(self, domain, username, password):
220+
def plaintext_login(self, username, password):
213221
return
214222

215223
def hash_login(self, domain, username, ntlm_hash):
@@ -223,14 +231,16 @@ def proto_flow(self):
223231
else:
224232
self.logger.debug("Created connection object")
225233
self.enum_host_info()
226-
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
234+
self.print_host_info()
235+
if self.login() or (self.username == "" and self.password == ""):
227236
if hasattr(self.args, "module") and self.args.module:
228237
self.load_modules()
229238
self.logger.debug("Calling modules")
230239
self.call_modules()
231240
else:
232241
self.logger.debug("Calling command arguments")
233242
self.call_cmd_args()
243+
self.disconnect()
234244

235245
def call_cmd_args(self):
236246
"""Calls all the methods specified by the command line arguments
@@ -376,7 +386,7 @@ def parse_credentials(self):
376386
if isfile(user):
377387
with open(user) as user_file:
378388
for line in user_file:
379-
if "\\" in line:
389+
if "\\" in line and len(line.split("\\")) == 2:
380390
domain_single, username_single = line.split("\\")
381391
else:
382392
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
@@ -466,8 +476,6 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
466476
return False
467477
if self.args.continue_on_success and owned:
468478
return False
469-
if hasattr(self.args, "delegate") and self.args.delegate:
470-
self.args.kerberos = True
471479

472480
if self.args.jitter:
473481
jitter = self.args.jitter
@@ -482,7 +490,7 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
482490

483491
with sem:
484492
if cred_type == "plaintext":
485-
if self.args.kerberos:
493+
if self.kerberos:
486494
self.logger.debug("Trying to authenticate using Kerberos")
487495
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
488496
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
@@ -495,7 +503,7 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
495503
self.logger.debug("Trying to authenticate using plaintext")
496504
return self.plaintext_login(username, secret)
497505
elif cred_type == "hash":
498-
if self.args.kerberos:
506+
if self.kerberos:
499507
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
500508
return self.hash_login(domain, username, secret)
501509
elif cred_type == "aesKey":
@@ -542,6 +550,14 @@ def login(self):
542550
self.logger.info("Successfully authenticated using Kerberos cache")
543551
return True
544552

553+
if self.args.pfx_cert or self.args.pfx_base64 or self.args.cert_pem:
554+
self.logger.debug("Trying to authenticate using Certificate pfx")
555+
if not self.args.username:
556+
self.logger.fail("You must specify a username when using certificate authentication")
557+
return False
558+
with sem:
559+
return pfx_auth(self)
560+
545561
if hasattr(self.args, "laps") and self.args.laps:
546562
self.logger.debug("Trying to authenticate using LAPS")
547563
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)

0 commit comments

Comments
 (0)