Skip to content

Commit 2a724d3

Browse files
authored
Merge branch 'main' into mssql-xp-shell-add-check
2 parents a8954f1 + 6d4fdfd commit 2a724d3

48 files changed

Lines changed: 2609 additions & 1253 deletions

Some content is hidden

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

.github/workflows/build-binaries.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
python-version: ["3.11"]
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

.github/workflows/build-zipapps.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ jobs:
1212
os: [ubuntu-latest, macOS-latest, windows-latest]
1313
python-version: ["3.8", "3.9", "3.10", "3.11"]
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

.github/workflows/lint.yml

Lines changed: 3 additions & 2 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,12 +12,12 @@ 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:
2122
python-version: 3.11
2223
cache: poetry

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ jobs:
1616
os: [ubuntu-latest]
1717
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1818
steps:
19-
- uses: actions/checkout@v3
19+
- uses: actions/checkout@v4
2020
- name: Install poetry
2121
run: |
2222
pipx install poetry
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

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ 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',
@@ -48,6 +49,7 @@ a = Analysis(
4849
'pywerview.cli.helpers',
4950
'pylnk3',
5051
'pypykatz',
52+
'pyNfsClient',
5153
'masky',
5254
'msldap',
5355
'msldap.connection',

nxc/cli.py

Lines changed: 3 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")

nxc/connection.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def __init__(self, args, db, target):
134134
# Authentication info
135135
self.password = ""
136136
self.username = ""
137-
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
137+
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
138138
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
139139
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
140140
self.admin_privs = False
@@ -157,7 +157,7 @@ def __init__(self, args, db, target):
157157
else:
158158
return
159159

160-
if self.args.kerberos:
160+
if self.kerberos:
161161
self.host = self.hostname
162162

163163
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}")
@@ -206,13 +206,16 @@ def print_host_info(self):
206206
def create_conn_obj(self):
207207
return
208208

209+
def disconnect(self):
210+
return
211+
209212
def check_if_admin(self):
210213
return
211214

212215
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
213216
return
214217

215-
def plaintext_login(self, domain, username, password):
218+
def plaintext_login(self, username, password):
216219
return
217220

218221
def hash_login(self, domain, username, ntlm_hash):
@@ -234,6 +237,7 @@ def proto_flow(self):
234237
else:
235238
self.logger.debug("Calling command arguments")
236239
self.call_cmd_args()
240+
self.disconnect()
237241

238242
def call_cmd_args(self):
239243
"""Calls all the methods specified by the command line arguments
@@ -469,8 +473,6 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
469473
return False
470474
if self.args.continue_on_success and owned:
471475
return False
472-
if hasattr(self.args, "delegate") and self.args.delegate:
473-
self.args.kerberos = True
474476

475477
if self.args.jitter:
476478
jitter = self.args.jitter
@@ -485,7 +487,7 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
485487

486488
with sem:
487489
if cred_type == "plaintext":
488-
if self.args.kerberos:
490+
if self.kerberos:
489491
self.logger.debug("Trying to authenticate using Kerberos")
490492
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
491493
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
@@ -498,7 +500,7 @@ def try_credentials(self, domain, username, owned, secret, cred_type, data=None)
498500
self.logger.debug("Trying to authenticate using plaintext")
499501
return self.plaintext_login(username, secret)
500502
elif cred_type == "hash":
501-
if self.args.kerberos:
503+
if self.kerberos:
502504
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
503505
return self.hash_login(domain, username, secret)
504506
elif cred_type == "aesKey":

nxc/database.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import sys
21
import configparser
32
import shutil
4-
from sqlalchemy import create_engine
5-
from sqlite3 import connect
3+
import sys
64
from os import mkdir
75
from os.path import exists
86
from os.path import join as path_join
7+
from pathlib import Path
8+
from sqlite3 import connect
9+
from threading import Lock
10+
11+
from sqlalchemy import create_engine, MetaData
12+
from sqlalchemy.exc import IllegalStateChangeError
13+
from sqlalchemy.orm import sessionmaker, scoped_session
914

1015
from nxc.loaders.protocolloader import ProtocolLoader
16+
from nxc.logger import nxc_logger
1117
from nxc.paths import WORKSPACE_DIR
1218

1319

@@ -45,24 +51,8 @@ def write_configfile(config, config_path):
4551
config.write(configfile)
4652

4753

48-
def create_workspace(workspace_name, p_loader=None):
49-
"""
50-
Create a new workspace with the given name.
51-
52-
Args:
53-
----
54-
workspace_name (str): The name of the workspace.
55-
56-
Returns:
57-
-------
58-
None
59-
"""
60-
if exists(path_join(WORKSPACE_DIR, workspace_name)):
61-
print(f"[-] Workspace {workspace_name} already exists")
62-
else:
63-
print(f"[*] Creating {workspace_name} workspace")
64-
mkdir(path_join(WORKSPACE_DIR, workspace_name))
65-
54+
def init_protocol_dbs(workspace_name, p_loader=None):
55+
"""Check for each protocol if the database exists, if not create it."""
6656
if p_loader is None:
6757
p_loader = ProtocolLoader()
6858
protocols = p_loader.get_protocols()
@@ -87,11 +77,71 @@ def create_workspace(workspace_name, p_loader=None):
8777
conn.close()
8878

8979

80+
def create_workspace(workspace_name, p_loader=None):
81+
"""
82+
Create a new workspace with the given name.
83+
84+
Args:
85+
----
86+
workspace_name (str): The name of the workspace.
87+
88+
Returns:
89+
-------
90+
None
91+
"""
92+
if exists(path_join(WORKSPACE_DIR, workspace_name)):
93+
print(f"[-] Workspace {workspace_name} already exists")
94+
else:
95+
print(f"[*] Creating {workspace_name} workspace")
96+
mkdir(path_join(WORKSPACE_DIR, workspace_name))
97+
98+
init_protocol_dbs(workspace_name, p_loader)
99+
100+
90101
def delete_workspace(workspace_name):
91102
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
92103
print(f"[*] Workspace {workspace_name} deleted")
93104

94105

95106
def initialize_db():
96107
if not exists(path_join(WORKSPACE_DIR, "default")):
97-
create_workspace("default")
108+
create_workspace("default")
109+
110+
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
111+
init_protocol_dbs("default")
112+
113+
114+
class BaseDB:
115+
def __init__(self, db_engine):
116+
self.db_engine = db_engine
117+
self.db_path = self.db_engine.url.database
118+
self.protocol = Path(self.db_path).stem.upper()
119+
self.metadata = MetaData()
120+
self.reflect_tables()
121+
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
122+
123+
session = scoped_session(session_factory)
124+
self.sess = session()
125+
self.lock = Lock()
126+
127+
def reflect_tables(self):
128+
raise NotImplementedError("Reflect tables not implemented")
129+
130+
def shutdown_db(self):
131+
try:
132+
self.sess.close()
133+
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
134+
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
135+
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
136+
except IllegalStateChangeError as e:
137+
nxc_logger.debug(f"Error while closing session db object: {e}")
138+
139+
def clear_database(self):
140+
for table in self.metadata.sorted_tables:
141+
self.db_execute(table.delete())
142+
143+
def db_execute(self, *args):
144+
self.lock.acquire()
145+
res = self.sess.execute(*args)
146+
self.lock.release()
147+
return res

0 commit comments

Comments
 (0)